root/trunk/WebCore/rendering/RenderListBox.cpp

Revision 38098, 20.8 KB (checked in by simon.fraser@apple.com, 4 weeks ago)

2008-11-04 Simon Fraser <simon.fraser@apple.com>

Reviewed by Dave Hyatt

https://bugs.webkit.org/show_bug.cgi?id=21941

Rename absolutePosition() to localToAbsolute(), and add the ability
to optionally take transforms into account (which will eventually be the
default behavior).

  • Property svn:eol-style set to native
Line 
1/*
2 * This file is part of the select element renderer in WebCore.
3 *
4 * Copyright (C) 2006, 2007, 2008 Apple Inc. All rights reserved.
5 *
6 * Redistribution and use in source and binary forms, with or without
7 * modification, are permitted provided that the following conditions
8 * are met:
9 *
10 * 1.  Redistributions of source code must retain the above copyright
11 *     notice, this list of conditions and the following disclaimer.
12 * 2.  Redistributions in binary form must reproduce the above copyright
13 *     notice, this list of conditions and the following disclaimer in the
14 *     documentation and/or other materials provided with the distribution.
15 * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
16 *     its contributors may be used to endorse or promote products derived
17 *     from this software without specific prior written permission.
18 *
19 * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
20 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
21 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
23 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
24 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
26 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
28 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 */
30
31#include "config.h"
32#include "RenderListBox.h"
33
34#include "AXObjectCache.h"
35#include "CSSStyleSelector.h"
36#include "Document.h"
37#include "EventHandler.h"
38#include "EventNames.h"
39#include "FocusController.h"
40#include "Frame.h"
41#include "FrameView.h"
42#include "GraphicsContext.h"
43#include "HTMLNames.h"
44#include "HTMLOptGroupElement.h"
45#include "HTMLOptionElement.h"
46#include "HTMLSelectElement.h"
47#include "HitTestResult.h"
48#include "Page.h"
49#include "RenderScrollbar.h"
50#include "RenderTheme.h"
51#include "RenderView.h"
52#include "Scrollbar.h"
53#include "SelectionController.h"
54#include "NodeRenderStyle.h"
55#include <math.h>
56
57using namespace std;
58
59namespace WebCore {
60
61using namespace HTMLNames;
62 
63const int rowSpacing = 1;
64
65const int optionsSpacingHorizontal = 2;
66
67const int minSize = 4;
68const int maxDefaultSize = 10;
69
70// FIXME: This hardcoded baselineAdjustment is what we used to do for the old
71// widget, but I'm not sure this is right for the new control.
72const int baselineAdjustment = 7;
73
74RenderListBox::RenderListBox(HTMLSelectElement* element)
75    : RenderBlock(element)
76    , m_optionsChanged(true)
77    , m_scrollToRevealSelectionAfterLayout(false)
78    , m_inAutoscroll(false)
79    , m_optionsWidth(0)
80    , m_indexOffset(0)
81{
82}
83
84RenderListBox::~RenderListBox()
85{
86    setHasVerticalScrollbar(false);
87}
88
89void RenderListBox::styleDidChange(RenderStyle::Diff diff, const RenderStyle* oldStyle)
90{
91    RenderBlock::styleDidChange(diff, oldStyle);
92    setReplaced(isInline());
93}
94
95void RenderListBox::updateFromElement()
96{
97    if (m_optionsChanged) {
98        const Vector<HTMLElement*>& listItems = static_cast<HTMLSelectElement*>(node())->listItems();
99        int size = numItems();
100       
101        float width = 0;
102        for (int i = 0; i < size; ++i) {
103            HTMLElement* element = listItems[i];
104            String text;
105            Font itemFont = style()->font();
106            if (element->hasTagName(optionTag))
107                text = static_cast<HTMLOptionElement*>(element)->optionText();
108            else if (element->hasTagName(optgroupTag)) {
109                text = static_cast<HTMLOptGroupElement*>(element)->groupLabelText();
110                FontDescription d = itemFont.fontDescription();
111                d.setWeight(d.bolderWeight());
112                itemFont = Font(d, itemFont.letterSpacing(), itemFont.wordSpacing());
113                itemFont.update(document()->styleSelector()->fontSelector());
114            }
115               
116            if (!text.isEmpty()) {
117                float textWidth = itemFont.floatWidth(TextRun(text.impl(), 0, 0, 0, false, false, false, false));
118                width = max(width, textWidth);
119            }
120        }
121        m_optionsWidth = static_cast<int>(ceilf(width));
122        m_optionsChanged = false;
123       
124        setHasVerticalScrollbar(true);
125
126        setNeedsLayoutAndPrefWidthsRecalc();
127    }
128}
129
130void RenderListBox::selectionChanged()
131{
132    repaint();
133    if (!m_inAutoscroll) {
134        if (m_optionsChanged || needsLayout())
135            m_scrollToRevealSelectionAfterLayout = true;
136        else
137            scrollToRevealSelection();
138    }
139   
140    if (AXObjectCache::accessibilityEnabled())
141        document()->axObjectCache()->selectedChildrenChanged(this);
142}
143
144void RenderListBox::layout()
145{
146    RenderBlock::layout();
147    if (m_scrollToRevealSelectionAfterLayout)
148        scrollToRevealSelection();
149}
150
151void RenderListBox::scrollToRevealSelection()
152{   
153    HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
154
155    m_scrollToRevealSelectionAfterLayout = false;
156
157    int firstIndex = select->activeSelectionStartListIndex();
158    if (firstIndex >= 0 && !listIndexIsVisible(select->activeSelectionEndListIndex()))
159        scrollToRevealElementAtListIndex(firstIndex);
160}
161
162void RenderListBox::calcPrefWidths()
163{
164    ASSERT(!m_optionsChanged);
165
166    m_minPrefWidth = 0;
167    m_maxPrefWidth = 0;
168
169    if (style()->width().isFixed() && style()->width().value() > 0)
170        m_minPrefWidth = m_maxPrefWidth = calcContentBoxWidth(style()->width().value());
171    else {
172        m_maxPrefWidth = m_optionsWidth + 2 * optionsSpacingHorizontal;
173        if (m_vBar)
174            m_maxPrefWidth += m_vBar->width();
175    }
176
177    if (style()->minWidth().isFixed() && style()->minWidth().value() > 0) {
178        m_maxPrefWidth = max(m_maxPrefWidth, calcContentBoxWidth(style()->minWidth().value()));
179        m_minPrefWidth = max(m_minPrefWidth, calcContentBoxWidth(style()->minWidth().value()));
180    } else if (style()->width().isPercent() || (style()->width().isAuto() && style()->height().isPercent()))
181        m_minPrefWidth = 0;
182    else
183        m_minPrefWidth = m_maxPrefWidth;
184
185    if (style()->maxWidth().isFixed() && style()->maxWidth().value() != undefinedLength) {
186        m_maxPrefWidth = min(m_maxPrefWidth, calcContentBoxWidth(style()->maxWidth().value()));
187        m_minPrefWidth = min(m_minPrefWidth, calcContentBoxWidth(style()->maxWidth().value()));
188    }
189
190    int toAdd = paddingLeft() + paddingRight() + borderLeft() + borderRight();
191    m_minPrefWidth += toAdd;
192    m_maxPrefWidth += toAdd;
193                               
194    setPrefWidthsDirty(false);
195}
196
197int RenderListBox::size() const
198{
199    int specifiedSize = static_cast<HTMLSelectElement*>(node())->size();
200    if (specifiedSize > 1)
201        return max(minSize, specifiedSize);
202    return min(max(minSize, numItems()), maxDefaultSize);
203}
204
205int RenderListBox::numVisibleItems() const
206{
207    // Only count fully visible rows. But don't return 0 even if only part of a row shows.
208    return max(1, (contentHeight() + rowSpacing) / itemHeight());
209}
210
211int RenderListBox::numItems() const
212{
213    return static_cast<HTMLSelectElement*>(node())->listItems().size();
214}
215
216int RenderListBox::listHeight() const
217{
218    return itemHeight() * numItems() - rowSpacing;
219}
220
221void RenderListBox::calcHeight()
222{
223    int toAdd = paddingTop() + paddingBottom() + borderTop() + borderBottom();
224 
225    int itemHeight = RenderListBox::itemHeight();
226    m_height = itemHeight * size() - rowSpacing + toAdd;
227   
228    RenderBlock::calcHeight();
229   
230    if (m_vBar) {
231        bool enabled = numVisibleItems() < numItems();
232        m_vBar->setEnabled(enabled);
233        m_vBar->setSteps(1, min(1, numVisibleItems() - 1), itemHeight);
234        m_vBar->setProportion(numVisibleItems(), numItems());
235        if (!enabled)
236            m_indexOffset = 0;
237    }
238}
239
240int RenderListBox::baselinePosition(bool b, bool isRootLineBox) const
241{
242    return height() + marginTop() + marginBottom() - baselineAdjustment;
243}
244
245IntRect RenderListBox::itemBoundingBoxRect(int tx, int ty, int index)
246{
247    return IntRect(tx + borderLeft() + paddingLeft(),
248                   ty + borderTop() + paddingTop() + itemHeight() * (index - m_indexOffset),
249                   contentWidth(), itemHeight());
250}
251   
252void RenderListBox::paintObject(PaintInfo& paintInfo, int tx, int ty)
253{
254    if (style()->visibility() != VISIBLE)
255        return;
256   
257    int listItemsSize = numItems();
258
259    if (paintInfo.phase == PaintPhaseForeground) {
260        int index = m_indexOffset;
261        while (index < listItemsSize && index <= m_indexOffset + numVisibleItems()) {
262            paintItemForeground(paintInfo, tx, ty, index);
263            index++;
264        }
265    }
266
267    // Paint the children.
268    RenderBlock::paintObject(paintInfo, tx, ty);
269
270    if (paintInfo.phase == PaintPhaseBlockBackground)
271        paintScrollbar(paintInfo, tx, ty);
272    else if (paintInfo.phase == PaintPhaseChildBlockBackground || paintInfo.phase == PaintPhaseChildBlockBackgrounds) {
273        int index = m_indexOffset;
274        while (index < listItemsSize && index <= m_indexOffset + numVisibleItems()) {
275            paintItemBackground(paintInfo, tx, ty, index);
276            index++;
277        }
278    }
279}
280
281void RenderListBox::paintScrollbar(PaintInfo& paintInfo, int tx, int ty)
282{
283    if (m_vBar) {
284        IntRect scrollRect(tx + width() - borderRight() - m_vBar->width(),
285                           ty + borderTop(),
286                           m_vBar->width(),
287                           height() - (borderTop() + borderBottom()));
288        m_vBar->setFrameRect(scrollRect);
289        m_vBar->paint(paintInfo.context, paintInfo.rect);
290    }
291}
292
293void RenderListBox::paintItemForeground(PaintInfo& paintInfo, int tx, int ty, int listIndex)
294{
295    HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
296    const Vector<HTMLElement*>& listItems = select->listItems();
297    HTMLElement* element = listItems[listIndex];
298
299    String itemText;
300    if (element->hasTagName(optionTag))
301        itemText = static_cast<HTMLOptionElement*>(element)->optionText();
302    else if (element->hasTagName(optgroupTag))
303        itemText = static_cast<HTMLOptGroupElement*>(element)->groupLabelText();
304       
305    // Determine where the item text should be placed
306    IntRect r = itemBoundingBoxRect(tx, ty, listIndex);
307    r.move(optionsSpacingHorizontal, style()->font().ascent());
308
309    RenderStyle* itemStyle = element->renderStyle();
310    if (!itemStyle)
311        itemStyle = style();
312   
313    Color textColor = element->renderStyle() ? element->renderStyle()->color() : style()->color();
314    if (element->hasTagName(optionTag) && static_cast<HTMLOptionElement*>(element)->selected()) {
315        if (document()->frame()->selection()->isFocusedAndActive() && document()->focusedNode() == node())
316            textColor = theme()->activeListBoxSelectionForegroundColor();
317        // Honor the foreground color for disabled items
318        else if (!element->disabled())
319            textColor = theme()->inactiveListBoxSelectionForegroundColor();
320    }
321
322    paintInfo.context->setFillColor(textColor);
323
324    Font itemFont = style()->font();
325    if (element->hasTagName(optgroupTag)) {
326        FontDescription d = itemFont.fontDescription();
327        d.setWeight(d.bolderWeight());
328        itemFont = Font(d, itemFont.letterSpacing(), itemFont.wordSpacing());
329        itemFont.update(document()->styleSelector()->fontSelector());
330    }
331    paintInfo.context->setFont(itemFont);
332   
333    unsigned length = itemText.length();
334    const UChar* string = itemText.characters();
335    TextRun textRun(string, length, 0, 0, 0, itemStyle->direction() == RTL, itemStyle->unicodeBidi() == Override, false, false);
336
337    // Draw the item text
338    if (itemStyle->visibility() != HIDDEN)
339        paintInfo.context->drawBidiText(textRun, r.location());
340}
341
342void RenderListBox::paintItemBackground(PaintInfo& paintInfo, int tx, int ty, int listIndex)
343{
344    HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
345    const Vector<HTMLElement*>& listItems = select->listItems();
346    HTMLElement* element = listItems[listIndex];
347
348    Color backColor;
349    if (element->hasTagName(optionTag) && static_cast<HTMLOptionElement*>(element)->selected()) {
350        if (document()->frame()->selection()->isFocusedAndActive() && document()->focusedNode() == node())
351            backColor = theme()->activeListBoxSelectionBackgroundColor();
352        else
353            backColor = theme()->inactiveListBoxSelectionBackgroundColor();
354    } else
355        backColor = element->renderStyle() ? element->renderStyle()->backgroundColor() : style()->backgroundColor();
356
357    // Draw the background for this list box item
358    if (!element->renderStyle() || element->renderStyle()->visibility() != HIDDEN) {
359        IntRect itemRect = itemBoundingBoxRect(tx, ty, listIndex);
360        itemRect.intersect(controlClipRect(tx, ty));
361        paintInfo.context->fillRect(itemRect, backColor);
362    }
363}
364
365bool RenderListBox::isPointInOverflowControl(HitTestResult& result, int _x, int _y, int _tx, int _ty)
366{
367    if (!m_vBar)
368        return false;
369
370    IntRect vertRect(_tx + width() - borderRight() - m_vBar->width(),
371                   _ty + borderTop() - borderTopExtra(),
372                   m_vBar->width(),
373                   height() + borderTopExtra() + borderBottomExtra() - borderTop() - borderBottom());
374
375    if (vertRect.contains(_x, _y)) {
376        result.setScrollbar(m_vBar.get());
377        return true;
378    }
379    return false;
380}
381
382int RenderListBox::listIndexAtOffset(int offsetX, int offsetY)
383{
384    if (!numItems())
385        return -1;
386
387    if (offsetY < borderTop() + paddingTop() || offsetY > height() - paddingBottom() - borderBottom())
388        return -1;
389
390    int scrollbarWidth = m_vBar ? m_vBar->width() : 0;
391    if (offsetX < borderLeft() + paddingLeft() || offsetX > width() - borderRight() - paddingRight() - scrollbarWidth)
392        return -1;
393
394    int newOffset = (offsetY - borderTop() - paddingTop()) / itemHeight() + m_indexOffset;
395    return newOffset < numItems() ? newOffset : -1;
396}
397
398void RenderListBox::panScroll(const IntPoint& panStartMousePosition)
399{
400    const int maxSpeed = 20;
401    const int iconRadius = 7;
402    const int speedReducer = 4;
403
404    // FIXME: This doesn't work correctly with transforms.
405    FloatPoint absOffset = localToAbsolute();
406
407    IntPoint currentMousePosition = document()->frame()->eventHandler()->currentMousePosition();
408    // We need to check if the current mouse position is out of the window. When the mouse is out of the window, the position is incoherent
409    static IntPoint previousMousePosition;
410    if (currentMousePosition.y() < 0)
411        currentMousePosition = previousMousePosition;
412    else
413        previousMousePosition = currentMousePosition;
414
415    int yDelta = currentMousePosition.y() - panStartMousePosition.y();
416
417   // If the point is too far from the center we limit the speed
418    yDelta = max(min(yDelta, maxSpeed), -maxSpeed);
419   
420    if(abs(yDelta) < iconRadius) // at the center we let the space for the icon
421        return;
422
423    if (yDelta > 0)
424        //offsetY = view()->viewHeight();
425        absOffset.move(0, listHeight());
426   else if (yDelta < 0)
427       yDelta--;
428
429    // Let's attenuate the speed
430    yDelta /= speedReducer;
431
432    IntPoint scrollPoint(0,0);
433    scrollPoint.setY(absOffset.y() + yDelta);
434    int newOffset = scrollToward(scrollPoint);
435    if (newOffset < 0) 
436        return;
437
438    m_inAutoscroll = true;
439    HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
440    select->updateListBoxSelection(!select->multiple());
441    m_inAutoscroll = false;
442}
443
444int RenderListBox::scrollToward(const IntPoint& destination)
445{
446    // FIXME: This doesn't work correctly with transforms.
447    FloatPoint absPos = localToAbsolute();
448    int offsetX = destination.x() - absPos.x();
449    int offsetY = destination.y() - absPos.y();
450
451    int rows = numVisibleItems();
452    int offset = m_indexOffset;
453   
454    if (offsetY < borderTop() + paddingTop() && scrollToRevealElementAtListIndex(offset - 1))
455        return offset - 1;
456   
457    if (offsetY > height() - paddingBottom() - borderBottom() && scrollToRevealElementAtListIndex(offset + rows))
458        return offset + rows - 1;
459   
460    return listIndexAtOffset(offsetX, offsetY);
461}
462
463void RenderListBox::autoscroll()
464{
465    IntPoint pos = document()->frame()->view()->windowToContents(document()->frame()->eventHandler()->currentMousePosition());
466
467    int endIndex = scrollToward(pos);
468    if (endIndex >= 0) {
469        HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
470        m_inAutoscroll = true;
471
472        if (!select->multiple())
473            select->setActiveSelectionAnchorIndex(endIndex);
474
475        select->setActiveSelectionEndIndex(endIndex);
476        select->updateListBoxSelection(!select->multiple());
477        m_inAutoscroll = false;
478    }
479}
480
481void RenderListBox::stopAutoscroll()
482{
483    static_cast<HTMLSelectElement*>(node())->listBoxOnChange();
484}
485
486bool RenderListBox::scrollToRevealElementAtListIndex(int index)
487{
488    if (index < 0 || index >= numItems() || listIndexIsVisible(index))
489        return false;
490
491    int newOffset;
492    if (index < m_indexOffset)
493        newOffset = index;
494    else
495        newOffset = index - numVisibleItems() + 1;
496
497    m_indexOffset = newOffset;
498    if (m_vBar)
499        m_vBar->setValue(m_indexOffset);
500
501    return true;
502}
503
504bool RenderListBox::listIndexIsVisible(int index)
505{   
506    return index >= m_indexOffset && index < m_indexOffset + numVisibleItems();
507}
508
509bool RenderListBox::scroll(ScrollDirection direction, ScrollGranularity granularity, float multiplier)
510{
511    return m_vBar && m_vBar->scroll(direction, granularity, multiplier);
512}
513
514void RenderListBox::valueChanged(unsigned listIndex)
515{
516    HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
517    select->setSelectedIndex(select->listToOptionIndex(listIndex));
518    select->onChange();
519}
520
521void RenderListBox::valueChanged(Scrollbar*)
522{
523    int newOffset = m_vBar->value();
524    if (newOffset != m_indexOffset) {
525        m_indexOffset = newOffset;
526        repaint();
527        // Fire the scroll DOM event.
528        EventTargetNodeCast(node())->dispatchEventForType(eventNames().scrollEvent, false, false);
529    }
530}
531
532int RenderListBox::itemHeight() const
533{
534    return style()->font().height() + rowSpacing;
535}
536
537int RenderListBox::verticalScrollbarWidth() const
538{
539    return m_vBar ? m_vBar->width() : 0;
540}
541
542// FIXME: We ignore padding in the vertical direction as far as these values are concerned, since that's
543// how the control currently paints.
544int RenderListBox::scrollWidth() const
545{
546    // There is no horizontal scrolling allowed.
547    return clientWidth();
548}
549
550int RenderListBox::scrollHeight() const
551{
552    return max(clientHeight(), listHeight());
553}
554
555int RenderListBox::scrollLeft() const
556{
557    return 0;
558}
559
560void RenderListBox::setScrollLeft(int)
561{
562}
563
564int RenderListBox::scrollTop() const
565{
566    return m_indexOffset * itemHeight();
567}
568
569void RenderListBox::setScrollTop(int newTop)
570{
571    // Determine an index and scroll to it.   
572    int index = newTop / itemHeight();
573    if (index < 0 || index >= numItems() || index == m_indexOffset)
574        return;
575    m_indexOffset = index;
576    if (m_vBar)
577        m_vBar->setValue(index);
578}
579
580IntRect RenderListBox::controlClipRect(int tx, int ty) const
581{
582    IntRect clipRect = contentBox();
583    clipRect.move(tx, ty);
584    return clipRect;
585}
586
587bool RenderListBox::isActive() const
588{
589    Page* page = document()->frame()->page();
590    return page && page->focusController()->isActive();
591}
592
593void RenderListBox::invalidateScrollbarRect(Scrollbar* scrollbar, const IntRect& rect)
594{
595    IntRect scrollRect = rect;
596    scrollRect.move(width() - borderRight() - scrollbar->width(), borderTop());
597    repaintRectangle(scrollRect);
598}
599
600bool RenderListBox::isScrollable() const
601{
602    if (numVisibleItems() < numItems())
603        return true;
604    return RenderObject::isScrollable();
605}
606
607PassRefPtr<Scrollbar> RenderListBox::createScrollbar()
608{
609    RefPtr<Scrollbar> widget;
610    bool hasCustomScrollbarStyle = style()->hasPseudoStyle(RenderStyle::SCROLLBAR);
611    if (hasCustomScrollbarStyle)
612        widget = RenderScrollbar::createCustomScrollbar(this, VerticalScrollbar, this);
613    else
614        widget = Scrollbar::createNativeScrollbar(this, VerticalScrollbar, SmallScrollbar);
615    document()->view()->addChild(widget.get());       
616    return widget.release();
617}
618
619void RenderListBox::destroyScrollbar()
620{
621    if (!m_vBar)
622        return;
623   
624    m_vBar->removeFromParent();
625    m_vBar->setClient(0);
626    m_vBar = 0;
627}
628
629void RenderListBox::setHasVerticalScrollbar(bool hasScrollbar)
630{
631    if (hasScrollbar == (m_vBar != 0))
632        return;
633
634    if (hasScrollbar)
635        m_vBar = createScrollbar();
636    else
637        destroyScrollbar();
638
639    if (m_vBar)
640        m_vBar->styleChanged();
641
642#if ENABLE(DASHBOARD_SUPPORT)
643    // Force an update since we know the scrollbars have changed things.
644    if (document()->hasDashboardRegions())
645        document()->setDashboardRegionsDirty(true);
646#endif
647}
648
649} // namespace WebCore
Note: See TracBrowser for help on using the browser.