1/*
2 * This file is part of the select element renderer in WebCore.
3 *
4 * Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies).
5 * Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011 Apple Inc. All rights reserved.
6 *               2009 Torch Mobile Inc. All rights reserved. (http://www.torchmobile.com/)
7 *
8 * This library is free software; you can redistribute it and/or
9 * modify it under the terms of the GNU Library General Public
10 * License as published by the Free Software Foundation; either
11 * version 2 of the License, or (at your option) any later version.
12 *
13 * This library is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
16 * Library General Public License for more details.
17 *
18 * You should have received a copy of the GNU Library General Public License
19 * along with this library; see the file COPYING.LIB.  If not, write to
20 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
21 * Boston, MA 02110-1301, USA.
22 *
23 */
24
25#include "config.h"
26#include "core/rendering/RenderMenuList.h"
27
28#include <math.h>
29#include "HTMLNames.h"
30#include "core/accessibility/AXMenuList.h"
31#include "core/accessibility/AXObjectCache.h"
32#include "core/css/CSSFontSelector.h"
33#include "core/css/resolver/StyleResolver.h"
34#include "core/dom/NodeRenderStyle.h"
35#include "core/html/HTMLOptGroupElement.h"
36#include "core/html/HTMLOptionElement.h"
37#include "core/html/HTMLSelectElement.h"
38#include "core/page/Chrome.h"
39#include "core/frame/Frame.h"
40#include "core/frame/FrameView.h"
41#include "core/page/Page.h"
42#include "core/rendering/RenderBR.h"
43#include "core/rendering/RenderScrollbar.h"
44#include "core/rendering/RenderTheme.h"
45#include "core/rendering/RenderView.h"
46#include "platform/fonts/FontCache.h"
47#include "platform/geometry/IntSize.h"
48
49using namespace std;
50
51namespace WebCore {
52
53using namespace HTMLNames;
54
55RenderMenuList::RenderMenuList(Element* element)
56    : RenderFlexibleBox(element)
57    , m_buttonText(0)
58    , m_innerBlock(0)
59    , m_optionsChanged(true)
60    , m_optionsWidth(0)
61    , m_lastActiveIndex(-1)
62    , m_popupIsVisible(false)
63{
64    ASSERT(element);
65    ASSERT(element->isHTMLElement());
66    ASSERT(element->hasTagName(HTMLNames::selectTag));
67}
68
69RenderMenuList::~RenderMenuList()
70{
71    if (m_popup)
72        m_popup->disconnectClient();
73    m_popup = 0;
74}
75
76void RenderMenuList::createInnerBlock()
77{
78    if (m_innerBlock) {
79        ASSERT(firstChild() == m_innerBlock);
80        ASSERT(!m_innerBlock->nextSibling());
81        return;
82    }
83
84    // Create an anonymous block.
85    ASSERT(!firstChild());
86    m_innerBlock = createAnonymousBlock();
87    adjustInnerStyle();
88    RenderFlexibleBox::addChild(m_innerBlock);
89}
90
91void RenderMenuList::adjustInnerStyle()
92{
93    RenderStyle* innerStyle = m_innerBlock->style();
94    innerStyle->setFlexGrow(1);
95    innerStyle->setFlexShrink(1);
96    // min-width: 0; is needed for correct shrinking.
97    // FIXME: Remove this line when https://bugs.webkit.org/show_bug.cgi?id=111790 is fixed.
98    innerStyle->setMinWidth(Length(0, Fixed));
99    // Use margin:auto instead of align-items:center to get safe centering, i.e.
100    // when the content overflows, treat it the same as align-items: flex-start.
101    // But we only do that for the cases where html.css would otherwise use center.
102    if (style()->alignItems() == AlignCenter) {
103        innerStyle->setMarginTop(Length());
104        innerStyle->setMarginBottom(Length());
105        innerStyle->setAlignSelf(AlignFlexStart);
106    }
107
108    innerStyle->setPaddingLeft(Length(RenderTheme::theme().popupInternalPaddingLeft(style()), Fixed));
109    innerStyle->setPaddingRight(Length(RenderTheme::theme().popupInternalPaddingRight(style()), Fixed));
110    innerStyle->setPaddingTop(Length(RenderTheme::theme().popupInternalPaddingTop(style()), Fixed));
111    innerStyle->setPaddingBottom(Length(RenderTheme::theme().popupInternalPaddingBottom(style()), Fixed));
112
113    if (m_optionStyle) {
114        if ((m_optionStyle->direction() != innerStyle->direction() || m_optionStyle->unicodeBidi() != innerStyle->unicodeBidi()))
115            m_innerBlock->setNeedsLayoutAndPrefWidthsRecalc();
116        innerStyle->setTextAlign(style()->isLeftToRightDirection() ? LEFT : RIGHT);
117        innerStyle->setDirection(m_optionStyle->direction());
118        innerStyle->setUnicodeBidi(m_optionStyle->unicodeBidi());
119    }
120}
121
122inline HTMLSelectElement* RenderMenuList::selectElement() const
123{
124    return toHTMLSelectElement(node());
125}
126
127void RenderMenuList::addChild(RenderObject* newChild, RenderObject* beforeChild)
128{
129    createInnerBlock();
130    m_innerBlock->addChild(newChild, beforeChild);
131    ASSERT(m_innerBlock == firstChild());
132
133    if (AXObjectCache* cache = document().existingAXObjectCache())
134        cache->childrenChanged(this);
135}
136
137void RenderMenuList::removeChild(RenderObject* oldChild)
138{
139    if (oldChild == m_innerBlock || !m_innerBlock) {
140        RenderFlexibleBox::removeChild(oldChild);
141        m_innerBlock = 0;
142    } else
143        m_innerBlock->removeChild(oldChild);
144}
145
146void RenderMenuList::styleDidChange(StyleDifference diff, const RenderStyle* oldStyle)
147{
148    RenderBlock::styleDidChange(diff, oldStyle);
149
150    if (m_buttonText)
151        m_buttonText->setStyle(style());
152    if (m_innerBlock) // RenderBlock handled updating the anonymous block's style.
153        adjustInnerStyle();
154
155    bool fontChanged = !oldStyle || oldStyle->font() != style()->font();
156    if (fontChanged)
157        updateOptionsWidth();
158}
159
160void RenderMenuList::updateOptionsWidth()
161{
162    float maxOptionWidth = 0;
163    const Vector<HTMLElement*>& listItems = selectElement()->listItems();
164    int size = listItems.size();
165    FontCachePurgePreventer fontCachePurgePreventer;
166
167    for (int i = 0; i < size; ++i) {
168        HTMLElement* element = listItems[i];
169        if (!element->hasTagName(optionTag))
170            continue;
171
172        String text = toHTMLOptionElement(element)->textIndentedToRespectGroupLabel();
173        applyTextTransform(style(), text, ' ');
174        if (RenderTheme::theme().popupOptionSupportsTextIndent()) {
175            // Add in the option's text indent.  We can't calculate percentage values for now.
176            float optionWidth = 0;
177            if (RenderStyle* optionStyle = element->renderStyle())
178                optionWidth += minimumValueForLength(optionStyle->textIndent(), 0, view());
179            if (!text.isEmpty())
180                optionWidth += style()->font().width(text);
181            maxOptionWidth = max(maxOptionWidth, optionWidth);
182        } else if (!text.isEmpty())
183            maxOptionWidth = max(maxOptionWidth, style()->font().width(text));
184    }
185
186    int width = static_cast<int>(ceilf(maxOptionWidth));
187    if (m_optionsWidth == width)
188        return;
189
190    m_optionsWidth = width;
191    if (parent())
192        setNeedsLayoutAndPrefWidthsRecalc();
193}
194
195void RenderMenuList::updateFromElement()
196{
197    if (m_optionsChanged) {
198        updateOptionsWidth();
199        m_optionsChanged = false;
200    }
201
202    if (m_popupIsVisible)
203        m_popup->updateFromElement();
204    else
205        setTextFromOption(selectElement()->selectedIndex());
206}
207
208void RenderMenuList::setTextFromOption(int optionIndex)
209{
210    HTMLSelectElement* select = selectElement();
211    const Vector<HTMLElement*>& listItems = select->listItems();
212    int size = listItems.size();
213
214    int i = select->optionToListIndex(optionIndex);
215    String text = emptyString();
216    if (i >= 0 && i < size) {
217        Element* element = listItems[i];
218        if (element->hasTagName(optionTag)) {
219            text = toHTMLOptionElement(element)->textIndentedToRespectGroupLabel();
220            m_optionStyle = element->renderStyle();
221        }
222    }
223
224    setText(text.stripWhiteSpace());
225    didUpdateActiveOption(optionIndex);
226}
227
228void RenderMenuList::setText(const String& s)
229{
230    if (s.isEmpty()) {
231        if (!m_buttonText || !m_buttonText->isBR()) {
232            if (m_buttonText)
233                m_buttonText->destroy();
234            m_buttonText = new RenderBR(&document());
235            m_buttonText->setStyle(style());
236            addChild(m_buttonText);
237        }
238    } else {
239        if (m_buttonText && !m_buttonText->isBR())
240            m_buttonText->setText(s.impl(), true);
241        else {
242            if (m_buttonText)
243                m_buttonText->destroy();
244            m_buttonText = new RenderText(&document(), s.impl());
245            m_buttonText->setStyle(style());
246            // We need to set the text explicitly though it was specified in the
247            // constructor because RenderText doesn't refer to the text
248            // specified in the constructor in a case of re-transforming.
249            m_buttonText->setText(s.impl(), true);
250            addChild(m_buttonText);
251        }
252        adjustInnerStyle();
253    }
254}
255
256String RenderMenuList::text() const
257{
258    return m_buttonText ? m_buttonText->text() : String();
259}
260
261LayoutRect RenderMenuList::controlClipRect(const LayoutPoint& additionalOffset) const
262{
263    // Clip to the intersection of the content box and the content box for the inner box
264    // This will leave room for the arrows which sit in the inner box padding,
265    // and if the inner box ever spills out of the outer box, that will get clipped too.
266    LayoutRect outerBox(additionalOffset.x() + borderLeft() + paddingLeft(),
267                   additionalOffset.y() + borderTop() + paddingTop(),
268                   contentWidth(),
269                   contentHeight());
270
271    LayoutRect innerBox(additionalOffset.x() + m_innerBlock->x() + m_innerBlock->paddingLeft(),
272                   additionalOffset.y() + m_innerBlock->y() + m_innerBlock->paddingTop(),
273                   m_innerBlock->contentWidth(),
274                   m_innerBlock->contentHeight());
275
276    return intersection(outerBox, innerBox);
277}
278
279void RenderMenuList::computeIntrinsicLogicalWidths(LayoutUnit& minLogicalWidth, LayoutUnit& maxLogicalWidth) const
280{
281    maxLogicalWidth = max(m_optionsWidth, RenderTheme::theme().minimumMenuListSize(style())) + m_innerBlock->paddingLeft() + m_innerBlock->paddingRight();
282    if (!style()->width().isPercent())
283        minLogicalWidth = maxLogicalWidth;
284}
285
286void RenderMenuList::computePreferredLogicalWidths()
287{
288    m_minPreferredLogicalWidth = 0;
289    m_maxPreferredLogicalWidth = 0;
290
291    if (style()->width().isFixed() && style()->width().value() > 0)
292        m_minPreferredLogicalWidth = m_maxPreferredLogicalWidth = adjustContentBoxLogicalWidthForBoxSizing(style()->width().value());
293    else
294        computeIntrinsicLogicalWidths(m_minPreferredLogicalWidth, m_maxPreferredLogicalWidth);
295
296    if (style()->minWidth().isFixed() && style()->minWidth().value() > 0) {
297        m_maxPreferredLogicalWidth = max(m_maxPreferredLogicalWidth, adjustContentBoxLogicalWidthForBoxSizing(style()->minWidth().value()));
298        m_minPreferredLogicalWidth = max(m_minPreferredLogicalWidth, adjustContentBoxLogicalWidthForBoxSizing(style()->minWidth().value()));
299    }
300
301    if (style()->maxWidth().isFixed()) {
302        m_maxPreferredLogicalWidth = min(m_maxPreferredLogicalWidth, adjustContentBoxLogicalWidthForBoxSizing(style()->maxWidth().value()));
303        m_minPreferredLogicalWidth = min(m_minPreferredLogicalWidth, adjustContentBoxLogicalWidthForBoxSizing(style()->maxWidth().value()));
304    }
305
306    LayoutUnit toAdd = borderAndPaddingWidth();
307    m_minPreferredLogicalWidth += toAdd;
308    m_maxPreferredLogicalWidth += toAdd;
309
310    clearPreferredLogicalWidthsDirty();
311}
312
313void RenderMenuList::showPopup()
314{
315    if (m_popupIsVisible)
316        return;
317
318    if (document().page()->chrome().hasOpenedPopup())
319        return;
320
321    // Create m_innerBlock here so it ends up as the first child.
322    // This is important because otherwise we might try to create m_innerBlock
323    // inside the showPopup call and it would fail.
324    createInnerBlock();
325    if (!m_popup)
326        m_popup = document().page()->chrome().createPopupMenu(*document().frame(), this);
327    m_popupIsVisible = true;
328
329    FloatQuad quad(localToAbsoluteQuad(FloatQuad(borderBoundingBox())));
330    IntSize size = pixelSnappedIntRect(frameRect()).size();
331    HTMLSelectElement* select = selectElement();
332    m_popup->show(quad, size, select->optionToListIndex(select->selectedIndex()));
333}
334
335void RenderMenuList::hidePopup()
336{
337    if (m_popup)
338        m_popup->hide();
339}
340
341void RenderMenuList::valueChanged(unsigned listIndex, bool fireOnChange)
342{
343    // Check to ensure a page navigation has not occurred while
344    // the popup was up.
345    Document& doc = toElement(node())->document();
346    if (&doc != doc.frame()->document())
347        return;
348
349    HTMLSelectElement* select = selectElement();
350    select->optionSelectedByUser(select->listToOptionIndex(listIndex), fireOnChange);
351}
352
353void RenderMenuList::listBoxSelectItem(int listIndex, bool allowMultiplySelections, bool shift, bool fireOnChangeNow)
354{
355    selectElement()->listBoxSelectItem(listIndex, allowMultiplySelections, shift, fireOnChangeNow);
356}
357
358bool RenderMenuList::multiple() const
359{
360    return selectElement()->multiple();
361}
362
363void RenderMenuList::didSetSelectedIndex(int listIndex)
364{
365    didUpdateActiveOption(selectElement()->listToOptionIndex(listIndex));
366}
367
368void RenderMenuList::didUpdateActiveOption(int optionIndex)
369{
370    if (!AXObjectCache::accessibilityEnabled() || !document().existingAXObjectCache())
371        return;
372
373    if (m_lastActiveIndex == optionIndex)
374        return;
375    m_lastActiveIndex = optionIndex;
376
377    HTMLSelectElement* select = selectElement();
378    int listIndex = select->optionToListIndex(optionIndex);
379    if (listIndex < 0 || listIndex >= static_cast<int>(select->listItems().size()))
380        return;
381    if (AXMenuList* menuList = toAXMenuList(document().axObjectCache()->get(this)))
382        menuList->didUpdateActiveOption(optionIndex);
383}
384
385String RenderMenuList::itemText(unsigned listIndex) const
386{
387    HTMLSelectElement* select = selectElement();
388    const Vector<HTMLElement*>& listItems = select->listItems();
389    if (listIndex >= listItems.size())
390        return String();
391
392    String itemString;
393    Element* element = listItems[listIndex];
394    if (isHTMLOptGroupElement(element))
395        itemString = toHTMLOptGroupElement(element)->groupLabelText();
396    else if (element->hasTagName(optionTag))
397        itemString = toHTMLOptionElement(element)->textIndentedToRespectGroupLabel();
398
399    applyTextTransform(style(), itemString, ' ');
400    return itemString;
401}
402
403String RenderMenuList::itemLabel(unsigned) const
404{
405    return String();
406}
407
408String RenderMenuList::itemIcon(unsigned) const
409{
410    return String();
411}
412
413String RenderMenuList::itemAccessibilityText(unsigned listIndex) const
414{
415    // Allow the accessible name be changed if necessary.
416    const Vector<HTMLElement*>& listItems = selectElement()->listItems();
417    if (listIndex >= listItems.size())
418        return String();
419    return listItems[listIndex]->fastGetAttribute(aria_labelAttr);
420}
421
422String RenderMenuList::itemToolTip(unsigned listIndex) const
423{
424    const Vector<HTMLElement*>& listItems = selectElement()->listItems();
425    if (listIndex >= listItems.size())
426        return String();
427    return listItems[listIndex]->title();
428}
429
430bool RenderMenuList::itemIsEnabled(unsigned listIndex) const
431{
432    const Vector<HTMLElement*>& listItems = selectElement()->listItems();
433    if (listIndex >= listItems.size())
434        return false;
435    HTMLElement* element = listItems[listIndex];
436    if (!element->hasTagName(optionTag))
437        return false;
438
439    bool groupEnabled = true;
440    if (Element* parentElement = element->parentElement()) {
441        if (isHTMLOptGroupElement(parentElement))
442            groupEnabled = !parentElement->isDisabledFormControl();
443    }
444    if (!groupEnabled)
445        return false;
446
447    return !element->isDisabledFormControl();
448}
449
450PopupMenuStyle RenderMenuList::itemStyle(unsigned listIndex) const
451{
452    const Vector<HTMLElement*>& listItems = selectElement()->listItems();
453    if (listIndex >= listItems.size()) {
454        // If we are making an out of bounds access, then we want to use the style
455        // of a different option element (index 0). However, if there isn't an option element
456        // before at index 0, we fall back to the menu's style.
457        if (!listIndex)
458            return menuStyle();
459
460        // Try to retrieve the style of an option element we know exists (index 0).
461        listIndex = 0;
462    }
463    HTMLElement* element = listItems[listIndex];
464
465    Color itemBackgroundColor;
466    bool itemHasCustomBackgroundColor;
467    getItemBackgroundColor(listIndex, itemBackgroundColor, itemHasCustomBackgroundColor);
468
469    RenderStyle* style = element->renderStyle() ? element->renderStyle() : element->computedStyle();
470    return style ? PopupMenuStyle(resolveColor(style, CSSPropertyColor), itemBackgroundColor, style->font(), style->visibility() == VISIBLE,
471        style->display() == NONE, style->textIndent(), style->direction(), isOverride(style->unicodeBidi()),
472        itemHasCustomBackgroundColor ? PopupMenuStyle::CustomBackgroundColor : PopupMenuStyle::DefaultBackgroundColor) : menuStyle();
473}
474
475void RenderMenuList::getItemBackgroundColor(unsigned listIndex, Color& itemBackgroundColor, bool& itemHasCustomBackgroundColor) const
476{
477    const Vector<HTMLElement*>& listItems = selectElement()->listItems();
478    if (listIndex >= listItems.size()) {
479        itemBackgroundColor = resolveColor(CSSPropertyBackgroundColor);
480        itemHasCustomBackgroundColor = false;
481        return;
482    }
483    HTMLElement* element = listItems[listIndex];
484
485    Color backgroundColor;
486    if (element->renderStyle())
487        backgroundColor = resolveColor(element->renderStyle(), CSSPropertyBackgroundColor);
488    itemHasCustomBackgroundColor = backgroundColor.isValid() && backgroundColor.alpha();
489    // If the item has an opaque background color, return that.
490    if (!backgroundColor.hasAlpha()) {
491        itemBackgroundColor = backgroundColor;
492        return;
493    }
494
495    // Otherwise, the item's background is overlayed on top of the menu background.
496    backgroundColor = resolveColor(CSSPropertyBackgroundColor).blend(backgroundColor);
497    if (!backgroundColor.hasAlpha()) {
498        itemBackgroundColor = backgroundColor;
499        return;
500    }
501
502    // If the menu background is not opaque, then add an opaque white background behind.
503    itemBackgroundColor = Color(Color::white).blend(backgroundColor);
504}
505
506PopupMenuStyle RenderMenuList::menuStyle() const
507{
508    const RenderObject* o = m_innerBlock ? m_innerBlock : this;
509    const RenderStyle* s = o->style();
510    return PopupMenuStyle(o->resolveColor(CSSPropertyColor), o->resolveColor(CSSPropertyBackgroundColor), s->font(), s->visibility() == VISIBLE,
511        s->display() == NONE, s->textIndent(), style()->direction(), isOverride(style()->unicodeBidi()));
512}
513
514HostWindow* RenderMenuList::hostWindow() const
515{
516    return document().view()->hostWindow();
517}
518
519PassRefPtr<Scrollbar> RenderMenuList::createScrollbar(ScrollableArea* scrollableArea, ScrollbarOrientation orientation, ScrollbarControlSize controlSize)
520{
521    RefPtr<Scrollbar> widget;
522    bool hasCustomScrollbarStyle = style()->hasPseudoStyle(SCROLLBAR);
523    if (hasCustomScrollbarStyle)
524        widget = RenderScrollbar::createCustomScrollbar(scrollableArea, orientation, this->node());
525    else
526        widget = Scrollbar::create(scrollableArea, orientation, controlSize);
527    return widget.release();
528}
529
530int RenderMenuList::clientInsetLeft() const
531{
532    return 0;
533}
534
535int RenderMenuList::clientInsetRight() const
536{
537    return 0;
538}
539
540LayoutUnit RenderMenuList::clientPaddingLeft() const
541{
542    return paddingLeft() + m_innerBlock->paddingLeft();
543}
544
545const int endOfLinePadding = 2;
546LayoutUnit RenderMenuList::clientPaddingRight() const
547{
548    if (style()->appearance() == MenulistPart || style()->appearance() == MenulistButtonPart) {
549        // For these appearance values, the theme applies padding to leave room for the
550        // drop-down button. But leaving room for the button inside the popup menu itself
551        // looks strange, so we return a small default padding to avoid having a large empty
552        // space appear on the side of the popup menu.
553        return endOfLinePadding;
554    }
555
556    // If the appearance isn't MenulistPart, then the select is styled (non-native), so
557    // we want to return the user specified padding.
558    return paddingRight() + m_innerBlock->paddingRight();
559}
560
561int RenderMenuList::listSize() const
562{
563    return selectElement()->listItems().size();
564}
565
566int RenderMenuList::selectedIndex() const
567{
568    HTMLSelectElement* select = selectElement();
569    return select->optionToListIndex(select->selectedIndex());
570}
571
572void RenderMenuList::popupDidHide()
573{
574    m_popupIsVisible = false;
575}
576
577bool RenderMenuList::itemIsSeparator(unsigned listIndex) const
578{
579    const Vector<HTMLElement*>& listItems = selectElement()->listItems();
580    return listIndex < listItems.size() && listItems[listIndex]->hasTagName(hrTag);
581}
582
583bool RenderMenuList::itemIsLabel(unsigned listIndex) const
584{
585    const Vector<HTMLElement*>& listItems = selectElement()->listItems();
586    return listIndex < listItems.size() && isHTMLOptGroupElement(listItems[listIndex]);
587}
588
589bool RenderMenuList::itemIsSelected(unsigned listIndex) const
590{
591    const Vector<HTMLElement*>& listItems = selectElement()->listItems();
592    if (listIndex >= listItems.size())
593        return false;
594    HTMLElement* element = listItems[listIndex];
595    return element->hasTagName(optionTag) && toHTMLOptionElement(element)->selected();
596}
597
598void RenderMenuList::setTextFromItem(unsigned listIndex)
599{
600    setTextFromOption(selectElement()->listToOptionIndex(listIndex));
601}
602
603FontSelector* RenderMenuList::fontSelector() const
604{
605    return document().styleEngine()->fontSelector();
606}
607
608}
609