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