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