OLD | NEW |
| (Empty) |
1 // Copyright (c) 2008, Google Inc. | |
2 // 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 #include "config.h" | |
31 | |
32 #pragma warning(push, 0) | |
33 #include "PopupMenu.h" | |
34 | |
35 #include "CharacterNames.h" | |
36 #include "ChromeClientWin.h" | |
37 #include "Document.h" | |
38 #include "Font.h" | |
39 #include "Frame.h" | |
40 #include "FontSelector.h" | |
41 #include "FramelessScrollView.h" | |
42 #include "GraphicsContext.h" | |
43 #include "IntRect.h" | |
44 #include "Page.h" | |
45 #include "PlatformKeyboardEvent.h" | |
46 #include "PlatformMouseEvent.h" | |
47 #include "PlatformScreen.h" | |
48 #include "PlatformScrollbar.h" | |
49 #include "PlatformWheelEvent.h" | |
50 #include "SystemTime.h" | |
51 #include "RenderBlock.h" | |
52 #include "RenderTheme.h" | |
53 #include "Widget.h" | |
54 #include "WidgetClientWin.h" | |
55 #pragma warning(pop) | |
56 | |
57 //#define LOG_ENABLE | |
58 #include "LogWin.h" | |
59 | |
60 using namespace WTF; | |
61 using namespace Unicode; | |
62 | |
63 using std::min; | |
64 using std::max; | |
65 | |
66 namespace WebCore { | |
67 | |
68 typedef unsigned long long TimeStamp; | |
69 | |
70 static const int kMaxVisibleRows = 20; | |
71 static const int kMaxHeight = 500; | |
72 static const int kBorderSize = 1; | |
73 static const TimeStamp kTypeAheadTimeoutMs = 1000; | |
74 | |
75 class PopupListBox; | |
76 | |
77 // This class holds a PopupListBox. Its sole purpose is to be able to draw | |
78 // a border around its child. All its paint/event handling is just forwarded | |
79 // to the child listBox (with the appropriate transforms). | |
80 class PopupContainer : public FramelessScrollView { | |
81 public: | |
82 static HWND Create(PopupMenuClient* client); | |
83 | |
84 // FramelessScrollView | |
85 virtual void paint(GraphicsContext* gc, const IntRect& rect); | |
86 virtual void hide(); | |
87 virtual bool handleMouseDownEvent(const PlatformMouseEvent& event); | |
88 virtual bool handleMouseMoveEvent(const PlatformMouseEvent& event); | |
89 virtual bool handleMouseReleaseEvent(const PlatformMouseEvent& event); | |
90 virtual bool handleWheelEvent(const PlatformWheelEvent& event); | |
91 virtual bool handleKeyEvent(const PlatformKeyboardEvent& event); | |
92 | |
93 // PopupContainer methods | |
94 | |
95 // Show the popup | |
96 void showPopup(FrameView* view); | |
97 | |
98 // Hide the popup. Do not call this directly: use client->hidePopup(). | |
99 void hidePopup(); | |
100 | |
101 // Compute size of widget and children. | |
102 void layout(); | |
103 | |
104 PopupListBox* listBox() const { return m_listBox.get(); } | |
105 | |
106 private: | |
107 PopupContainer(PopupMenuClient* client); | |
108 ~PopupContainer(); | |
109 | |
110 // Paint the border. | |
111 void paintBorder(GraphicsContext* gc, const IntRect& rect); | |
112 | |
113 RefPtr<PopupListBox> m_listBox; | |
114 }; | |
115 | |
116 // This class uses WebCore code to paint and handle events for a drop-down list | |
117 // box ("combobox" on Windows). | |
118 class PopupListBox : public FramelessScrollView { | |
119 public: | |
120 // FramelessScrollView | |
121 virtual void paint(GraphicsContext* gc, const IntRect& rect); | |
122 virtual bool handleMouseDownEvent(const PlatformMouseEvent& event); | |
123 virtual bool handleMouseMoveEvent(const PlatformMouseEvent& event); | |
124 virtual bool handleMouseReleaseEvent(const PlatformMouseEvent& event); | |
125 virtual bool handleWheelEvent(const PlatformWheelEvent& event); | |
126 virtual bool handleKeyEvent(const PlatformKeyboardEvent& event); | |
127 | |
128 // PopupListBox methods | |
129 | |
130 // Show the popup | |
131 void showPopup(); | |
132 | |
133 // Hide the popup. Do not call this directly: use client->hidePopup(). | |
134 void hidePopup(); | |
135 | |
136 // Update our internal list to match the client. | |
137 void updateFromElement(); | |
138 | |
139 // Free any allocated resources used in a particular popup session. | |
140 void clear(); | |
141 | |
142 // Set the index of the option that is displayed in the <select> widget in t
he page | |
143 void setOriginalIndex(int index); | |
144 | |
145 // Get the index of the item that the user is currently moused over or has s
elected with | |
146 // the keyboard. This is not the same as the original index, since the user
has not yet | |
147 // accepted this input. | |
148 int selectedIndex() const { return m_selectedIndex; } | |
149 | |
150 // Move selection down/up the given number of items, scrolling if necessary. | |
151 // Positive is down. The resulting index will be clamped to the range | |
152 // [0, numItems), and non-option items will be skipped. | |
153 void adjustSelectedIndex(int delta); | |
154 | |
155 // Returns the number of items in the list. | |
156 int numItems() const { return static_cast<int>(m_items.size()); } | |
157 | |
158 void setBaseWidth(int width) | |
159 { | |
160 m_baseWidth = width; | |
161 } | |
162 | |
163 // Compute size of widget and children. | |
164 void layout(); | |
165 | |
166 private: | |
167 friend class PopupContainer; | |
168 | |
169 // A type of List Item | |
170 enum ListItemType { | |
171 TYPE_OPTION, | |
172 TYPE_GROUP, | |
173 TYPE_SEPARATOR | |
174 }; | |
175 | |
176 // A item (represented by <option> or <optgroup>) in the <select> widget. | |
177 struct ListItem { | |
178 ListItem(const String& label, ListItemType type) | |
179 : label(label.copy()), type(type), y(0) {} | |
180 String label; | |
181 ListItemType type; | |
182 int y; // y offset of this item, relative to the top of the popup. | |
183 }; | |
184 | |
185 PopupListBox(PopupMenuClient* client) | |
186 : m_originalIndex(0) | |
187 , m_selectedIndex(0) | |
188 , m_visibleRows(0) | |
189 , m_popupClient(client) | |
190 , m_repeatingChar(0) | |
191 , m_lastCharTime(0) | |
192 , m_acceptOnAbandon(false) | |
193 { | |
194 setScrollbarsMode(ScrollbarAlwaysOff); | |
195 } | |
196 | |
197 ~PopupListBox() | |
198 { | |
199 clear(); | |
200 } | |
201 | |
202 void disconnectClient() { m_popupClient = 0; } | |
203 | |
204 // Closes the popup | |
205 void abandon(); | |
206 // Select an index in the list, scrolling if necessary. | |
207 void selectIndex(int index); | |
208 // Accepts the selected index as the value to be displayed in the <select> w
idget on | |
209 // the web page, and closes the popup. | |
210 void acceptIndex(int index); | |
211 | |
212 // Returns true if the selection can be changed to index. | |
213 // Disabled items, or labels cannot be selected. | |
214 bool isSelectableItem(int index); | |
215 | |
216 // Scrolls to reveal the given index. | |
217 void scrollToRevealRow(int index); | |
218 void scrollToRevealSelection() { scrollToRevealRow(m_selectedIndex); } | |
219 | |
220 // Invalidates the row at the given index. | |
221 void invalidateRow(int index); | |
222 | |
223 // Gets the height of a row. | |
224 int getRowHeight(int index); | |
225 // Get the bounds of a row. | |
226 IntRect getRowBounds(int index); | |
227 | |
228 // Converts a point to an index of the row the point is over | |
229 int pointToRowIndex(const IntPoint& point); | |
230 | |
231 // Paint an individual row | |
232 void paintRow(GraphicsContext* gc, const IntRect& rect, int rowIndex); | |
233 | |
234 // Test if the given point is within the bounds of the popup window. | |
235 bool isPointInBounds(const IntPoint& point); | |
236 | |
237 // Called when the user presses a text key. Does a prefix-search of the ite
ms. | |
238 void typeAheadFind(const PlatformKeyboardEvent& event); | |
239 | |
240 // Returns the font to use for the given row | |
241 Font getRowFont(int index); | |
242 | |
243 // This is the index of the item marked as "selected" - i.e. displayed in th
e widget on the | |
244 // page. | |
245 int m_originalIndex; | |
246 | |
247 // This is the index of the item that the user is hovered over or has select
ed using the | |
248 // keyboard in the list. They have not confirmed this selection by clicking
or pressing | |
249 // enter yet however. | |
250 int m_selectedIndex; | |
251 | |
252 // True if we should accept the selectedIndex as chosen, even if the popup | |
253 // is "abandoned". This is used for keyboard navigation, where we want the | |
254 // selection to change immediately. | |
255 bool m_acceptOnAbandon; | |
256 | |
257 // This is the number of rows visible in the popup. The maximum number visib
le at a time is | |
258 // defined as being kMaxVisibleRows. For a scrolled popup, this can be thoug
ht of as the | |
259 // page size in data units. | |
260 int m_visibleRows; | |
261 | |
262 // Our suggested width, not including scrollbar. | |
263 int m_baseWidth; | |
264 | |
265 // A list of the options contained within the <select> | |
266 Vector<ListItem*> m_items; | |
267 | |
268 // The <select> PopupMenuClient that opened us. | |
269 PopupMenuClient* m_popupClient; | |
270 | |
271 // The scrollbar which has mouse capture. Mouse events go straight to this | |
272 // if non-NULL. | |
273 RefPtr<PlatformScrollbar> m_capturingScrollbar; | |
274 | |
275 // The last scrollbar that the mouse was over. Used for mouseover highlight
s. | |
276 RefPtr<PlatformScrollbar> m_lastScrollbarUnderMouse; | |
277 | |
278 // The string the user has typed so far into the popup. Used for typeAheadFi
nd. | |
279 String m_typedString; | |
280 | |
281 // The char the user has hit repeatedly. Used for typeAheadFind. | |
282 UChar m_repeatingChar; | |
283 | |
284 // The last time the user hit a key. Used for typeAheadFind. | |
285 TimeStamp m_lastCharTime; | |
286 }; | |
287 | |
288 static PlatformMouseEvent constructRelativeMouseEvent(const PlatformMouseEvent&
e, | |
289 FrameView* parent, | |
290 FrameView* child) | |
291 { | |
292 IntPoint pos = parent->convertSelfToChild(child, e.pos()); | |
293 | |
294 // FIXME(beng): This is a horrible hack since PlatformWheelEvent has no sett
ers for x/y. | |
295 // Need to add setters and get patch back upstream to webkit so
urce. | |
296 PlatformMouseEvent relativeEvent = e; | |
297 IntPoint& relativePos = const_cast<IntPoint&>(relativeEvent.pos()); | |
298 relativePos.setX(pos.x()); | |
299 relativePos.setY(pos.y()); | |
300 return relativeEvent; | |
301 } | |
302 | |
303 static PlatformWheelEvent constructRelativeWheelEvent(const PlatformWheelEvent&
e, | |
304 FrameView* parent, | |
305 FrameView* child) | |
306 { | |
307 IntPoint pos = parent->convertSelfToChild(child, e.pos()); | |
308 | |
309 // FIXME(beng): This is a horrible hack since PlatformWheelEvent has no sett
ers for x/y. | |
310 // Need to add setters and get patch back upstream to webkit so
urce. | |
311 PlatformWheelEvent relativeEvent = e; | |
312 IntPoint& relativePos = const_cast<IntPoint&>(relativeEvent.pos()); | |
313 relativePos.setX(pos.x()); | |
314 relativePos.setY(pos.y()); | |
315 return relativeEvent; | |
316 } | |
317 | |
318 /////////////////////////////////////////////////////////////////////////////// | |
319 // PopupContainer implementation | |
320 | |
321 // Get a pointer to the PopupContainer instance for the m_popup HWND, since we | |
322 // can't augment the PopupMenu class (above the portability layer). We store the | |
323 // PopupContainer in the HWND member, which is fairly hacky. | |
324 static PopupContainer* popupWindow(HWND popup) | |
325 { | |
326 return reinterpret_cast<PopupContainer*>(popup); | |
327 } | |
328 | |
329 // static | |
330 HWND PopupContainer::Create(PopupMenuClient* client) | |
331 { | |
332 PopupContainer* container = new PopupContainer(client); | |
333 return reinterpret_cast<HWND>(container); | |
334 } | |
335 | |
336 PopupContainer::PopupContainer(PopupMenuClient* client) | |
337 : m_listBox(new PopupListBox(client)) | |
338 { | |
339 // FrameViews are created with a refcount of 1 so it needs releasing after w
e | |
340 // assign it to a RefPtr. | |
341 m_listBox->deref(); | |
342 | |
343 setScrollbarsMode(ScrollbarAlwaysOff); | |
344 } | |
345 | |
346 PopupContainer::~PopupContainer() | |
347 { | |
348 if (m_listBox) | |
349 removeChild(m_listBox.get()); | |
350 } | |
351 | |
352 void PopupContainer::showPopup(FrameView* view) | |
353 { | |
354 // Pre-layout, our size matches the <select> dropdown control. | |
355 int selectHeight = frameGeometry().height(); | |
356 | |
357 // Lay everything out to figure out our preferred size, then tell the view's | |
358 // WidgetClient about it. It should assign us a client. | |
359 layout(); | |
360 | |
361 WidgetClientWin* widgetClient = | |
362 static_cast<WidgetClientWin*>(view->client()); | |
363 ChromeClientWin* chromeClient = | |
364 static_cast<ChromeClientWin*>(view->frame()->page()->chrome()->client())
; | |
365 if (widgetClient && chromeClient) { | |
366 // If the popup would extend past the bottom of the screen, open upwards | |
367 // instead. | |
368 FloatRect screen = screenRect(view); | |
369 IntRect widgetRect = chromeClient->windowToScreen(frameGeometry()); | |
370 if (widgetRect.bottom() > static_cast<int>(screen.bottom())) | |
371 widgetRect.move(0, -(widgetRect.height() + selectHeight)); | |
372 | |
373 widgetClient->popupOpened(this, widgetRect); | |
374 } | |
375 | |
376 // Must get called after we have a client and containingWindow. | |
377 addChild(m_listBox.get()); | |
378 | |
379 // Enable scrollbars after the listbox is inserted into the hierarchy, so | |
380 // it has a proper WidgetClient. | |
381 m_listBox->setVScrollbarMode(ScrollbarAuto); | |
382 | |
383 m_listBox->scrollToRevealSelection(); | |
384 | |
385 invalidate(); | |
386 } | |
387 | |
388 void PopupContainer::hidePopup() | |
389 { | |
390 invalidate(); | |
391 | |
392 m_listBox->disconnectClient(); | |
393 removeChild(m_listBox.get()); | |
394 | |
395 if (client()) | |
396 static_cast<WidgetClientWin*>(client())->popupClosed(this); | |
397 } | |
398 | |
399 void PopupContainer::layout() | |
400 { | |
401 m_listBox->layout(); | |
402 | |
403 // Place the listbox within our border. | |
404 m_listBox->move(kBorderSize, kBorderSize); | |
405 | |
406 // Size ourselves to contain listbox + border. | |
407 resize(m_listBox->width() + kBorderSize*2, m_listBox->height() + kBorderSize
*2); | |
408 | |
409 invalidate(); | |
410 } | |
411 | |
412 bool PopupContainer::handleMouseDownEvent(const PlatformMouseEvent& event) | |
413 { | |
414 return m_listBox->handleMouseDownEvent( | |
415 constructRelativeMouseEvent(event, this, m_listBox.get())); | |
416 } | |
417 | |
418 bool PopupContainer::handleMouseMoveEvent(const PlatformMouseEvent& event) | |
419 { | |
420 return m_listBox->handleMouseMoveEvent( | |
421 constructRelativeMouseEvent(event, this, m_listBox.get())); | |
422 } | |
423 | |
424 bool PopupContainer::handleMouseReleaseEvent(const PlatformMouseEvent& event) | |
425 { | |
426 return m_listBox->handleMouseReleaseEvent( | |
427 constructRelativeMouseEvent(event, this, m_listBox.get())); | |
428 } | |
429 | |
430 bool PopupContainer::handleWheelEvent(const PlatformWheelEvent& event) | |
431 { | |
432 return m_listBox->handleWheelEvent( | |
433 constructRelativeWheelEvent(event, this, m_listBox.get())); | |
434 } | |
435 | |
436 bool PopupContainer::handleKeyEvent(const PlatformKeyboardEvent& event) | |
437 { | |
438 return m_listBox->handleKeyEvent(event); | |
439 } | |
440 | |
441 void PopupContainer::hide() { | |
442 m_listBox->abandon(); | |
443 } | |
444 | |
445 void PopupContainer::paint(GraphicsContext* gc, const IntRect& rect) | |
446 { | |
447 // adjust coords for scrolled frame | |
448 IntRect r = intersection(rect, frameGeometry()); | |
449 int tx = x(); | |
450 int ty = y(); | |
451 | |
452 r.move(-tx, -ty); | |
453 | |
454 gc->translate(static_cast<float>(tx), static_cast<float>(ty)); | |
455 m_listBox->paint(gc, r); | |
456 gc->translate(-static_cast<float>(tx), -static_cast<float>(ty)); | |
457 | |
458 paintBorder(gc, rect); | |
459 } | |
460 | |
461 void PopupContainer::paintBorder(GraphicsContext* gc, const IntRect& rect) | |
462 { | |
463 // FIXME(mpcomplete): where do we get the border color from? | |
464 Color borderColor(127, 157, 185); | |
465 | |
466 gc->setStrokeStyle(NoStroke); | |
467 gc->setFillColor(borderColor); | |
468 | |
469 int tx = x(); | |
470 int ty = y(); | |
471 | |
472 // top, left, bottom, right | |
473 gc->drawRect(IntRect(tx, ty, width(), kBorderSize)); | |
474 gc->drawRect(IntRect(tx, ty, kBorderSize, height())); | |
475 gc->drawRect(IntRect(tx, ty + height() - kBorderSize, width(), kBorderSize))
; | |
476 gc->drawRect(IntRect(tx + width() - kBorderSize, ty, kBorderSize, height()))
; | |
477 } | |
478 | |
479 | |
480 /////////////////////////////////////////////////////////////////////////////// | |
481 // PopupListBox implementation | |
482 | |
483 bool PopupListBox::handleMouseDownEvent(const PlatformMouseEvent& event) | |
484 { | |
485 PlatformScrollbar* scrollbar = scrollbarUnderMouse(event); | |
486 if (scrollbar) { | |
487 m_capturingScrollbar = scrollbar; | |
488 m_capturingScrollbar->handleMousePressEvent(event); | |
489 return true; | |
490 } | |
491 | |
492 if (!isPointInBounds(event.pos())) | |
493 abandon(); | |
494 | |
495 return true; | |
496 } | |
497 | |
498 bool PopupListBox::handleMouseMoveEvent(const PlatformMouseEvent& event) | |
499 { | |
500 if (m_capturingScrollbar) { | |
501 m_capturingScrollbar->handleMouseMoveEvent(event); | |
502 return true; | |
503 } | |
504 | |
505 PlatformScrollbar* scrollbar = scrollbarUnderMouse(event); | |
506 if (m_lastScrollbarUnderMouse != scrollbar) { | |
507 // Send mouse exited to the old scrollbar. | |
508 if (m_lastScrollbarUnderMouse) | |
509 m_lastScrollbarUnderMouse->handleMouseOutEvent(event); | |
510 m_lastScrollbarUnderMouse = scrollbar; | |
511 } | |
512 | |
513 if (scrollbar) { | |
514 scrollbar->handleMouseMoveEvent(event); | |
515 return true; | |
516 } | |
517 | |
518 if (!isPointInBounds(event.pos())) | |
519 return false; | |
520 | |
521 selectIndex(pointToRowIndex(event.pos())); | |
522 return true; | |
523 } | |
524 | |
525 bool PopupListBox::handleMouseReleaseEvent(const PlatformMouseEvent& event) | |
526 { | |
527 if (m_capturingScrollbar) { | |
528 m_capturingScrollbar->handleMouseReleaseEvent(event); | |
529 m_capturingScrollbar = 0; | |
530 return true; | |
531 } | |
532 | |
533 if (!isPointInBounds(event.pos())) | |
534 return true; | |
535 | |
536 acceptIndex(pointToRowIndex(event.pos())); | |
537 return true; | |
538 } | |
539 | |
540 bool PopupListBox::handleWheelEvent(const PlatformWheelEvent& event) | |
541 { | |
542 if (!isPointInBounds(event.pos())) { | |
543 abandon(); | |
544 return true; | |
545 } | |
546 | |
547 // Pass it off to the scroll view. | |
548 // Sadly, WebCore devs don't understand the whole "const" thing. | |
549 wheelEvent(const_cast<PlatformWheelEvent&>(event)); | |
550 return true; | |
551 } | |
552 | |
553 bool PopupListBox::handleKeyEvent(const PlatformKeyboardEvent& event) | |
554 { | |
555 if (event.type() == PlatformKeyboardEvent::KeyUp) | |
556 return true; | |
557 | |
558 if (numItems() == 0 && event.windowsVirtualKeyCode() != VK_ESCAPE) | |
559 return true; | |
560 | |
561 int oldIndex = m_selectedIndex; | |
562 | |
563 switch (event.windowsVirtualKeyCode()) { | |
564 case VK_ESCAPE: | |
565 abandon(); // may delete this | |
566 return true; | |
567 case VK_RETURN: | |
568 acceptIndex(m_selectedIndex); // may delete this | |
569 return true; | |
570 case VK_UP: | |
571 adjustSelectedIndex(-1); | |
572 break; | |
573 case VK_DOWN: | |
574 adjustSelectedIndex(1); | |
575 break; | |
576 case VK_PRIOR: | |
577 adjustSelectedIndex(-m_visibleRows); | |
578 break; | |
579 case VK_NEXT: | |
580 adjustSelectedIndex(m_visibleRows); | |
581 break; | |
582 case VK_HOME: | |
583 adjustSelectedIndex(-m_selectedIndex); | |
584 break; | |
585 case VK_END: | |
586 adjustSelectedIndex(m_items.size()); | |
587 break; | |
588 default: | |
589 if (!event.ctrlKey() && !event.altKey() && !event.metaKey() && | |
590 isPrintableChar(event.windowsVirtualKeyCode())) { | |
591 typeAheadFind(event); | |
592 } | |
593 break; | |
594 } | |
595 | |
596 if (m_originalIndex != m_selectedIndex) { | |
597 // Keyboard events should update the selection immediately (but we don't | |
598 // want to fire the onchange event until the popup is closed, to match | |
599 // IE). We change the original index so we revert to that when the | |
600 // popup is closed. | |
601 m_acceptOnAbandon = true; | |
602 setOriginalIndex(m_selectedIndex); | |
603 m_popupClient->setTextFromItem(m_selectedIndex); | |
604 } | |
605 | |
606 return true; | |
607 } | |
608 | |
609 // From HTMLSelectElement.cpp | |
610 static String stripLeadingWhiteSpace(const String& string) | |
611 { | |
612 int length = string.length(); | |
613 int i; | |
614 for (i = 0; i < length; ++i) | |
615 if (string[i] != noBreakSpace && | |
616 (string[i] <= 0x7F ? !isspace(string[i]) : (direction(string[i]) !=
WhiteSpaceNeutral))) | |
617 break; | |
618 | |
619 return string.substring(i, length - i); | |
620 } | |
621 | |
622 // From HTMLSelectElement.cpp, with modifications | |
623 void PopupListBox::typeAheadFind(const PlatformKeyboardEvent& event) | |
624 { | |
625 TimeStamp now = static_cast<TimeStamp>(currentTime() * 1000.0f); | |
626 TimeStamp delta = now - m_lastCharTime; | |
627 | |
628 m_lastCharTime = now; | |
629 | |
630 UChar c = event.windowsVirtualKeyCode(); | |
631 | |
632 String prefix; | |
633 int searchStartOffset = 1; | |
634 if (delta > kTypeAheadTimeoutMs) { | |
635 m_typedString = prefix = String(&c, 1); | |
636 m_repeatingChar = c; | |
637 } else { | |
638 m_typedString.append(c); | |
639 | |
640 if (c == m_repeatingChar) | |
641 // The user is likely trying to cycle through all the items starting
with this character, so just search on the character | |
642 prefix = String(&c, 1); | |
643 else { | |
644 m_repeatingChar = 0; | |
645 prefix = m_typedString; | |
646 searchStartOffset = 0; | |
647 } | |
648 } | |
649 | |
650 int itemCount = numItems(); | |
651 int index = (m_selectedIndex + searchStartOffset) % itemCount; | |
652 for (int i = 0; i < itemCount; i++, index = (index + 1) % itemCount) { | |
653 if (!isSelectableItem(index)) | |
654 continue; | |
655 | |
656 if (stripLeadingWhiteSpace(m_items[index]->label).startsWith(prefix, fal
se)) { | |
657 selectIndex(index); | |
658 return; | |
659 } | |
660 } | |
661 } | |
662 | |
663 void PopupListBox::paint(GraphicsContext* gc, const IntRect& rect) | |
664 { | |
665 // adjust coords for scrolled frame | |
666 IntRect r = intersection(rect, frameGeometry()); | |
667 int tx = x() - contentsX(); | |
668 int ty = y() - contentsY(); | |
669 | |
670 r.move(-tx, -ty); | |
671 | |
672 LOG(("PopupListBox::paint [%d,%d] [r: %d,%d,%d,%d]", tx, ty, | |
673 r.x(), r.y(), r.width(), r.height())); | |
674 | |
675 // set clip rect to match revised damage rect | |
676 gc->save(); | |
677 gc->translate(static_cast<float>(tx), static_cast<float>(ty)); | |
678 gc->clip(r); | |
679 | |
680 // TODO(mpcomplete): Can we optimize scrolling to not require repainting the | |
681 // entire window? Should we? | |
682 for (int i = 0; i < numItems(); ++i) | |
683 paintRow(gc, r, i); | |
684 | |
685 // Special case for an empty popup. | |
686 if (numItems() == 0) | |
687 gc->fillRect(r, Color::white); | |
688 | |
689 gc->restore(); | |
690 | |
691 ScrollView::paint(gc, rect); | |
692 } | |
693 | |
694 static RenderStyle* getPopupClientStyleForRow(PopupMenuClient* client, int rowIn
dex) { | |
695 RenderStyle* style = client->itemStyle(rowIndex); | |
696 if (!style) | |
697 style = client->clientStyle(); | |
698 return style; | |
699 } | |
700 | |
701 void PopupListBox::paintRow(GraphicsContext* gc, const IntRect& rect, int rowInd
ex) | |
702 { | |
703 // This code is based largely on RenderListBox::paint* methods. | |
704 | |
705 IntRect rowRect = getRowBounds(rowIndex); | |
706 if (!rowRect.intersects(rect)) | |
707 return; | |
708 | |
709 RenderStyle* style = getPopupClientStyleForRow(m_popupClient, rowIndex); | |
710 | |
711 // Paint background | |
712 Color backColor, textColor; | |
713 if (rowIndex == m_selectedIndex) { | |
714 backColor = theme()->activeListBoxSelectionBackgroundColor(); | |
715 textColor = theme()->activeListBoxSelectionForegroundColor(); | |
716 } else { | |
717 backColor = m_popupClient->itemBackgroundColor(rowIndex); | |
718 textColor = style->color(); | |
719 } | |
720 | |
721 // If we have a transparent background, make sure it has a color to blend | |
722 // against. | |
723 if (backColor.hasAlpha()) | |
724 gc->fillRect(rowRect, Color::white); | |
725 | |
726 gc->fillRect(rowRect, backColor); | |
727 gc->setFillColor(textColor); | |
728 | |
729 LOG(("paintRow %d, [%d, %d, %d, %d] %x on %x", rowIndex, | |
730 rowRect.x(), rowRect.y(), rowRect.width(), rowRect.height(), | |
731 textColor.rgb(), backColor.rgb())); | |
732 | |
733 Font itemFont = getRowFont(rowIndex); | |
734 gc->setFont(itemFont); | |
735 | |
736 // Bunch of shit to deal with RTL text... | |
737 String itemText = m_popupClient->itemText(rowIndex); | |
738 unsigned length = itemText.length(); | |
739 const UChar* str = itemText.characters(); | |
740 | |
741 TextRun textRun(str, length, false, 0, 0, style->direction() == RTL, style->
unicodeBidi() == Override); | |
742 | |
743 // Draw the item text | |
744 // TODO(ojan): http://b/1210481 We should get the padding of individual opti
on elements. | |
745 rowRect.move(theme()->popupInternalPaddingLeft(style), itemFont.ascent()); | |
746 if (style->direction() == RTL) { | |
747 // Right-justify the text for RTL style. | |
748 rowRect.move(rowRect.width() - itemFont.width(textRun) - | |
749 2 * theme()->popupInternalPaddingLeft(style), 0); | |
750 } | |
751 gc->drawBidiText(textRun, rowRect.location()); | |
752 } | |
753 | |
754 Font PopupListBox::getRowFont(int rowIndex) | |
755 { | |
756 Font itemFont = m_popupClient->itemStyle(rowIndex)->font(); | |
757 if (m_popupClient->itemIsLabel(rowIndex)) { | |
758 // Bold-ify labels (ie, an <optgroup> heading). | |
759 FontDescription d = itemFont.fontDescription(); | |
760 d.setWeight(FontWeightBold); | |
761 Font font(d, itemFont.letterSpacing(), itemFont.wordSpacing()); | |
762 font.update(0); | |
763 return font; | |
764 } | |
765 | |
766 return itemFont; | |
767 } | |
768 | |
769 void PopupListBox::abandon() | |
770 { | |
771 RefPtr<PopupListBox> keepAlive(this); | |
772 | |
773 m_selectedIndex = m_originalIndex; | |
774 | |
775 if (m_acceptOnAbandon) | |
776 m_popupClient->valueChanged(m_selectedIndex); | |
777 | |
778 // valueChanged may have torn down the popup! | |
779 if (m_popupClient) | |
780 m_popupClient->hidePopup(); | |
781 } | |
782 | |
783 int PopupListBox::pointToRowIndex(const IntPoint& point) | |
784 { | |
785 int y = contentsY() + point.y(); | |
786 | |
787 // TODO(mpcomplete): binary search if perf matters. | |
788 for (int i = 0; i < numItems(); ++i) { | |
789 if (y < m_items[i]->y) | |
790 return i-1; | |
791 } | |
792 | |
793 // Last item? | |
794 if (y < contentsHeight()) | |
795 return m_items.size()-1; | |
796 | |
797 return -1; | |
798 } | |
799 | |
800 void PopupListBox::acceptIndex(int index) | |
801 { | |
802 ASSERT(index >= 0 && index < numItems()); | |
803 | |
804 if (isSelectableItem(index)) { | |
805 RefPtr<PopupListBox> keepAlive(this); | |
806 | |
807 // Tell the <select> PopupMenuClient what index was selected, and hide o
urself. | |
808 m_popupClient->valueChanged(index); | |
809 | |
810 // valueChanged may have torn down the popup! | |
811 if (m_popupClient) | |
812 m_popupClient->hidePopup(); | |
813 } | |
814 } | |
815 | |
816 void PopupListBox::selectIndex(int index) | |
817 { | |
818 ASSERT(index >= 0 && index < numItems()); | |
819 | |
820 if (index != m_selectedIndex && isSelectableItem(index)) { | |
821 invalidateRow(m_selectedIndex); | |
822 m_selectedIndex = index; | |
823 invalidateRow(m_selectedIndex); | |
824 | |
825 scrollToRevealSelection(); | |
826 } | |
827 } | |
828 | |
829 void PopupListBox::setOriginalIndex(int index) | |
830 { | |
831 m_originalIndex = m_selectedIndex = index; | |
832 } | |
833 | |
834 int PopupListBox::getRowHeight(int index) | |
835 { | |
836 RenderStyle* style; | |
837 if ((index < 0) || (!(style = m_popupClient->itemStyle(index)))) | |
838 style = m_popupClient->clientStyle(); | |
839 return style->font().height(); | |
840 } | |
841 | |
842 IntRect PopupListBox::getRowBounds(int index) | |
843 { | |
844 if (index >= 0) { | |
845 return IntRect(0, m_items[index]->y, visibleWidth(), getRowHeight(index)
); | |
846 } else { | |
847 return IntRect(0, 0, visibleWidth(), getRowHeight(index)); | |
848 } | |
849 } | |
850 | |
851 void PopupListBox::invalidateRow(int index) | |
852 { | |
853 if (index < 0) | |
854 return; | |
855 | |
856 updateContents(getRowBounds(index)); | |
857 } | |
858 | |
859 void PopupListBox::scrollToRevealRow(int index) | |
860 { | |
861 if (index < 0) | |
862 return; | |
863 | |
864 IntRect rowRect = getRowBounds(index); | |
865 | |
866 if (rowRect.y() < contentsY()) { | |
867 // Row is above current scroll position, scroll up. | |
868 ScrollView::setContentsPos(0, rowRect.y()); | |
869 } else if (rowRect.bottom() > contentsY() + visibleHeight()) { | |
870 // Row is below current scroll position, scroll down. | |
871 ScrollView::setContentsPos(0, rowRect.bottom() - visibleHeight()); | |
872 } | |
873 } | |
874 | |
875 bool PopupListBox::isSelectableItem(int index) { | |
876 return m_items[index]->type == TYPE_OPTION && | |
877 m_popupClient->itemIsEnabled(index); | |
878 } | |
879 | |
880 void PopupListBox::adjustSelectedIndex(int delta) | |
881 { | |
882 int targetIndex = m_selectedIndex + delta; | |
883 targetIndex = min(max(targetIndex, 0), numItems() - 1); | |
884 if (!isSelectableItem(targetIndex)) { | |
885 // We didn't land on an option. Try to find one. | |
886 // We try to select the closest index to target, prioritizing any in | |
887 // the range [current, target]. | |
888 | |
889 int dir = delta > 0 ? 1 : -1; | |
890 int testIndex = m_selectedIndex; | |
891 int bestIndex = m_selectedIndex; | |
892 bool passedTarget = false; | |
893 while (testIndex >= 0 && testIndex < numItems()) { | |
894 if (isSelectableItem(testIndex)) | |
895 bestIndex = testIndex; | |
896 if (testIndex == targetIndex) | |
897 passedTarget = true; | |
898 if (passedTarget && bestIndex != m_selectedIndex) | |
899 break; | |
900 | |
901 testIndex += dir; | |
902 } | |
903 | |
904 // Pick the best index, which may mean we don't change. | |
905 targetIndex = bestIndex; | |
906 } | |
907 | |
908 // Select the new index, and ensure its visible. We do this regardless of | |
909 // whether the selection changed to ensure keyboard events always bring the | |
910 // selection into view. | |
911 selectIndex(targetIndex); | |
912 scrollToRevealSelection(); | |
913 } | |
914 | |
915 void PopupListBox::updateFromElement() | |
916 { | |
917 // It happens when pressing a key to jump to an item, then use tab or | |
918 // mouse to get away from the select box. In that case, updateFromElement | |
919 // is called before abandon, which causes discarding of the select result.
| |
920 if (m_acceptOnAbandon) { | |
921 m_popupClient->valueChanged(m_selectedIndex); | |
922 m_acceptOnAbandon = false; | |
923 } | |
924 | |
925 clear(); | |
926 | |
927 int size = m_popupClient->listSize(); | |
928 for (int i = 0; i < size; ++i) { | |
929 ListItemType type; | |
930 if (m_popupClient->itemIsSeparator(i)) | |
931 type = PopupListBox::TYPE_SEPARATOR; | |
932 else if (m_popupClient->itemIsLabel(i)) | |
933 type = PopupListBox::TYPE_GROUP; | |
934 else | |
935 type = PopupListBox::TYPE_OPTION; | |
936 m_items.append(new ListItem(m_popupClient->itemText(i), type)); | |
937 } | |
938 | |
939 m_selectedIndex = m_popupClient->selectedIndex(); | |
940 setOriginalIndex(m_selectedIndex); | |
941 | |
942 layout(); | |
943 } | |
944 | |
945 void PopupListBox::layout() | |
946 { | |
947 // Size our child items. | |
948 int baseWidth = 0; | |
949 int paddingWidth = 0; | |
950 int y = 0; | |
951 for (int i = 0; i < numItems(); ++i) { | |
952 Font itemFont = getRowFont(i); | |
953 | |
954 // Place the item vertically. | |
955 m_items[i]->y = y; | |
956 y += itemFont.height(); | |
957 | |
958 // Ensure the popup is wide enough to fit this item. | |
959 String text = m_popupClient->itemText(i); | |
960 if (!text.isEmpty()) { | |
961 int width = itemFont.width(TextRun(text)); | |
962 baseWidth = max(baseWidth, width); | |
963 } | |
964 RenderStyle* style = getPopupClientStyleForRow(m_popupClient, i); | |
965 // TODO(ojan): http://b/1210481 We should get the padding of individual
option elements. | |
966 paddingWidth = max(paddingWidth, | |
967 theme()->popupInternalPaddingLeft(style) + theme()->popupInternalPad
dingRight(style)); | |
968 } | |
969 | |
970 int windowHeight = 0; | |
971 m_visibleRows = min(numItems(), kMaxVisibleRows); | |
972 for (int i = 0; i < m_visibleRows; ++i) { | |
973 int rowHeight = getRowHeight(i); | |
974 if (windowHeight + rowHeight > kMaxHeight) { | |
975 m_visibleRows = i; | |
976 break; | |
977 } | |
978 | |
979 windowHeight += rowHeight; | |
980 } | |
981 | |
982 if (windowHeight == 0) | |
983 windowHeight = min(getRowHeight(-1), kMaxHeight); | |
984 | |
985 // Set our widget and scrollable contents sizes. | |
986 int scrollbarWidth = 0; | |
987 if (m_visibleRows < numItems()) | |
988 scrollbarWidth = PlatformScrollbar::verticalScrollbarWidth(); | |
989 | |
990 int windowWidth = baseWidth + scrollbarWidth + paddingWidth; | |
991 int contentWidth = baseWidth; | |
992 | |
993 if (windowWidth < m_baseWidth) { | |
994 windowWidth = m_baseWidth; | |
995 contentWidth = m_baseWidth - scrollbarWidth - paddingWidth; | |
996 } else { | |
997 m_baseWidth = baseWidth; | |
998 } | |
999 | |
1000 resize(windowWidth, windowHeight); | |
1001 resizeContents(contentWidth, getRowBounds(numItems() - 1).bottom()); | |
1002 scrollToRevealSelection(); | |
1003 | |
1004 invalidate(); | |
1005 } | |
1006 | |
1007 void PopupListBox::clear() | |
1008 { | |
1009 for (Vector<ListItem*>::iterator it = m_items.begin(); it != m_items.end();
++it) | |
1010 delete *it; | |
1011 m_items.clear(); | |
1012 } | |
1013 | |
1014 bool PopupListBox::isPointInBounds(const IntPoint& point) | |
1015 { | |
1016 return numItems() != 0 && IntRect(0, 0, width(), height()).contains(point); | |
1017 } | |
1018 | |
1019 | |
1020 /////////////////////////////////////////////////////////////////////////////// | |
1021 // PopupMenu implementation | |
1022 // | |
1023 // Note: you cannot add methods to this class, since it is defined above the | |
1024 // portability layer. To access methods and properties on the | |
1025 // popup widgets, use |popupWindow| above. | |
1026 | |
1027 PopupMenu::PopupMenu(PopupMenuClient* client) | |
1028 : m_popupClient(client) | |
1029 , m_popup(0) | |
1030 , m_wasClicked(false) | |
1031 { | |
1032 } | |
1033 | |
1034 PopupMenu::~PopupMenu() | |
1035 { | |
1036 hide(); | |
1037 } | |
1038 | |
1039 void PopupMenu::show(const IntRect& r, FrameView* v, int index) | |
1040 { | |
1041 m_popup = PopupContainer::Create(client()); | |
1042 | |
1043 PopupContainer* popup = popupWindow(m_popup); | |
1044 // The rect is the size of the select box. It's usually larger than we need. | |
1045 // subtract border size so that usually the container will be displayed | |
1046 // exactly the same width as the select box. | |
1047 popup->listBox()->setBaseWidth(max(r.width() - kBorderSize * 2, 0)); | |
1048 | |
1049 updateFromElement(); | |
1050 | |
1051 // We set the selected item in updateFromElement(), and disregard the | |
1052 // index passed into this function (same as Webkit's PopupMenuWin.cpp) | |
1053 // TODO(ericroman): make sure this is correct, and add an assertion. | |
1054 // DCHECK(popupWindow(m_popup)->listBox()->selectedIndex() == index); | |
1055 | |
1056 // Convert point to main window coords. | |
1057 IntPoint location = v->contentsToWindow(r.location()); | |
1058 | |
1059 // Move it below the select widget. | |
1060 location.move(0, r.height()); | |
1061 | |
1062 IntRect popupRect(location, r.size()); | |
1063 popup->setFrameGeometry(popupRect); | |
1064 popup->showPopup(v); | |
1065 } | |
1066 | |
1067 void PopupMenu::hide() | |
1068 { | |
1069 if (m_popup) { | |
1070 PopupContainer* popup = popupWindow(m_popup); | |
1071 popup->hidePopup(); | |
1072 popup->deref(); | |
1073 m_popup = 0; | |
1074 } | |
1075 } | |
1076 | |
1077 void PopupMenu::updateFromElement() | |
1078 { | |
1079 popupWindow(m_popup)->listBox()->updateFromElement(); | |
1080 } | |
1081 | |
1082 bool PopupMenu::itemWritingDirectionIsNatural() | |
1083 { | |
1084 return false; | |
1085 } | |
1086 | |
1087 bool PopupMenu::up(unsigned lines) | |
1088 { | |
1089 popupWindow(m_popup)->listBox()->adjustSelectedIndex(-static_cast<int>(lines
)); | |
1090 return true; | |
1091 } | |
1092 | |
1093 bool PopupMenu::down(unsigned lines) | |
1094 { | |
1095 popupWindow(m_popup)->listBox()->adjustSelectedIndex(static_cast<int>(lines)
); | |
1096 return true; | |
1097 } | |
1098 | |
1099 int PopupMenu::focusedIndex() const | |
1100 { | |
1101 return popupWindow(m_popup)->listBox()->selectedIndex(); | |
1102 } | |
1103 | |
1104 void PopupMenu::valueChanged(Scrollbar*) | |
1105 { | |
1106 // FIXME | |
1107 } | |
1108 | |
1109 WebCore::IntRect PopupMenu::windowClipRect() const | |
1110 { | |
1111 // FIXME | |
1112 return WebCore::IntRect(); | |
1113 } | |
1114 | |
1115 } // namespace WebCore | |
OLD | NEW |