Home | History | Annotate | Download | only in rendering
      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 
     45 using namespace std;
     46 
     47 namespace WebCore {
     48 
     49 using namespace HTMLNames;
     50 
     51 RenderMenuList::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 
     62 RenderMenuList::~RenderMenuList()
     63 {
     64     if (m_popup)
     65         m_popup->disconnectClient();
     66     m_popup = 0;
     67 }
     68 
     69 void 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 
     84 void 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 
    109 void RenderMenuList::addChild(RenderObject* newChild, RenderObject* beforeChild)
    110 {
    111     createInnerBlock();
    112     m_innerBlock->addChild(newChild, beforeChild);
    113     ASSERT(m_innerBlock == firstChild());
    114 }
    115 
    116 void 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 
    125 void 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 
    139 void 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 
    172 void 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 
    185 void 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 
    204 void 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 
    228 String RenderMenuList::text() const
    229 {
    230     return m_buttonText ? m_buttonText->text() : 0;
    231 }
    232 
    233 IntRect 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 
    251 void 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 
    281 void 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 
    304 void RenderMenuList::hidePopup()
    305 {
    306     if (m_popup)
    307         m_popup->hide();
    308 }
    309 
    310 void 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)
    323 void 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 
    329 bool RenderMenuList::multiple()
    330 {
    331     SelectElement* select = toSelectElement(static_cast<Element*>(node()));
    332     return select->multiple();
    333 }
    334 #endif
    335 
    336 void 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 
    348 String 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 
    362 String RenderMenuList::itemLabel(unsigned) const
    363 {
    364     return String();
    365 }
    366 
    367 String RenderMenuList::itemIcon(unsigned) const
    368 {
    369     return String();
    370 }
    371 
    372 String 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 
    383 String 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 
    393 bool 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 
    414 PopupMenuStyle 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 
    434 Color 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 
    458 PopupMenuStyle 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 
    464 HostWindow* RenderMenuList::hostWindow() const
    465 {
    466     return document()->view()->hostWindow();
    467 }
    468 
    469 PassRefPtr<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 
    480 int RenderMenuList::clientInsetLeft() const
    481 {
    482     return 0;
    483 }
    484 
    485 int RenderMenuList::clientInsetRight() const
    486 {
    487     return 0;
    488 }
    489 
    490 int RenderMenuList::clientPaddingLeft() const
    491 {
    492     return paddingLeft() + m_innerBlock->paddingLeft();
    493 }
    494 
    495 const int endOfLinePadding = 2;
    496 int 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 
    511 int RenderMenuList::listSize() const
    512 {
    513     SelectElement* select = toSelectElement(static_cast<Element*>(node()));
    514     return select->listItems().size();
    515 }
    516 
    517 int RenderMenuList::selectedIndex() const
    518 {
    519     SelectElement* select = toSelectElement(static_cast<Element*>(node()));
    520     return select->optionToListIndex(select->selectedIndex());
    521 }
    522 
    523 void RenderMenuList::popupDidHide()
    524 {
    525     m_popupIsVisible = false;
    526 }
    527 
    528 bool 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 
    538 bool 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 
    548 bool 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 
    560 void RenderMenuList::setTextFromItem(unsigned listIndex)
    561 {
    562     SelectElement* select = toSelectElement(static_cast<Element*>(node()));
    563     setTextFromOption(select->listToOptionIndex(listIndex));
    564 }
    565 
    566 FontSelector* RenderMenuList::fontSelector() const
    567 {
    568     return document()->styleSelector()->fontSelector();
    569 }
    570 
    571 }
    572