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