| /* |
| * This file is part of the select element layoutObject in WebCore. |
| * |
| * Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies). |
| * Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011 Apple Inc. |
| * All rights reserved. |
| * (C) 2009 Torch Mobile Inc. All rights reserved. |
| * (http://www.torchmobile.com/) |
| * |
| * This library is free software; you can redistribute it and/or |
| * modify it under the terms of the GNU Library General Public |
| * License as published by the Free Software Foundation; either |
| * version 2 of the License, or (at your option) any later version. |
| * |
| * This library is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| * Library General Public License for more details. |
| * |
| * You should have received a copy of the GNU Library General Public License |
| * along with this library; see the file COPYING.LIB. If not, write to |
| * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, |
| * Boston, MA 02110-1301, USA. |
| * |
| */ |
| |
| #include "core/layout/LayoutMenuList.h" |
| |
| #include "core/dom/AXObjectCache.h" |
| #include "core/dom/NodeComputedStyle.h" |
| #include "core/frame/FrameView.h" |
| #include "core/html/HTMLOptionElement.h" |
| #include "core/html/HTMLSelectElement.h" |
| #include "core/layout/LayoutText.h" |
| #include "core/layout/LayoutTheme.h" |
| #include "platform/text/PlatformLocale.h" |
| #include <math.h> |
| |
| namespace blink { |
| |
| LayoutMenuList::LayoutMenuList(Element* element) |
| : LayoutFlexibleBox(element), |
| m_buttonText(nullptr), |
| m_innerBlock(nullptr), |
| m_isEmpty(false), |
| m_hasUpdatedActiveOption(false), |
| m_innerBlockHeight(LayoutUnit()), |
| m_optionsWidth(0), |
| m_lastActiveIndex(-1) { |
| ASSERT(isHTMLSelectElement(element)); |
| } |
| |
| LayoutMenuList::~LayoutMenuList() {} |
| |
| // FIXME: Instead of this hack we should add a ShadowRoot to <select> with no |
| // insertion point to prevent children from rendering. |
| bool LayoutMenuList::isChildAllowed(LayoutObject* object, |
| const ComputedStyle&) const { |
| return object->isAnonymous() && !object->isLayoutFullScreen(); |
| } |
| |
| void LayoutMenuList::createInnerBlock() { |
| if (m_innerBlock) { |
| ASSERT(firstChild() == m_innerBlock); |
| ASSERT(!m_innerBlock->nextSibling()); |
| return; |
| } |
| |
| // Create an anonymous block. |
| ASSERT(!firstChild()); |
| m_innerBlock = createAnonymousBlock(); |
| |
| m_buttonText = new LayoutText(&document(), StringImpl::empty()); |
| // We need to set the text explicitly though it was specified in the |
| // constructor because LayoutText doesn't refer to the text |
| // specified in the constructor in a case of re-transforming. |
| m_buttonText->setStyle(mutableStyle()); |
| m_innerBlock->addChild(m_buttonText); |
| |
| adjustInnerStyle(); |
| LayoutFlexibleBox::addChild(m_innerBlock); |
| } |
| |
| void LayoutMenuList::adjustInnerStyle() { |
| ComputedStyle& innerStyle = m_innerBlock->mutableStyleRef(); |
| innerStyle.setFlexGrow(1); |
| innerStyle.setFlexShrink(1); |
| // min-width: 0; is needed for correct shrinking. |
| innerStyle.setMinWidth(Length(0, Fixed)); |
| // Use margin:auto instead of align-items:center to get safe centering, i.e. |
| // when the content overflows, treat it the same as align-items: flex-start. |
| // But we only do that for the cases where html.css would otherwise use |
| // center. |
| if (style()->alignItemsPosition() == ItemPositionCenter) { |
| innerStyle.setMarginTop(Length()); |
| innerStyle.setMarginBottom(Length()); |
| innerStyle.setAlignSelfPosition(ItemPositionFlexStart); |
| } |
| |
| Length paddingStart = |
| Length(LayoutTheme::theme().popupInternalPaddingStart(styleRef()), Fixed); |
| Length paddingEnd = Length(LayoutTheme::theme().popupInternalPaddingEnd( |
| frameView()->getHostWindow(), styleRef()), |
| Fixed); |
| innerStyle.setPaddingLeft(styleRef().direction() == TextDirection::kLtr |
| ? paddingStart |
| : paddingEnd); |
| innerStyle.setPaddingRight(styleRef().direction() == TextDirection::kLtr |
| ? paddingEnd |
| : paddingStart); |
| innerStyle.setPaddingTop( |
| Length(LayoutTheme::theme().popupInternalPaddingTop(styleRef()), Fixed)); |
| innerStyle.setPaddingBottom(Length( |
| LayoutTheme::theme().popupInternalPaddingBottom(styleRef()), Fixed)); |
| |
| if (m_optionStyle) { |
| if ((m_optionStyle->direction() != innerStyle.direction() || |
| m_optionStyle->getUnicodeBidi() != innerStyle.getUnicodeBidi())) |
| m_innerBlock->setNeedsLayoutAndPrefWidthsRecalcAndFullPaintInvalidation( |
| LayoutInvalidationReason::StyleChange); |
| innerStyle.setTextAlign(style()->isLeftToRightDirection() |
| ? ETextAlign::kLeft |
| : ETextAlign::kRight); |
| innerStyle.setDirection(m_optionStyle->direction()); |
| innerStyle.setUnicodeBidi(m_optionStyle->getUnicodeBidi()); |
| } |
| } |
| |
| HTMLSelectElement* LayoutMenuList::selectElement() const { |
| return toHTMLSelectElement(node()); |
| } |
| |
| void LayoutMenuList::addChild(LayoutObject* newChild, |
| LayoutObject* beforeChild) { |
| m_innerBlock->addChild(newChild, beforeChild); |
| ASSERT(m_innerBlock == firstChild()); |
| |
| if (AXObjectCache* cache = document().existingAXObjectCache()) |
| cache->childrenChanged(this); |
| } |
| |
| void LayoutMenuList::removeChild(LayoutObject* oldChild) { |
| if (oldChild == m_innerBlock || !m_innerBlock) { |
| LayoutFlexibleBox::removeChild(oldChild); |
| m_innerBlock = nullptr; |
| } else { |
| m_innerBlock->removeChild(oldChild); |
| } |
| } |
| |
| void LayoutMenuList::styleDidChange(StyleDifference diff, |
| const ComputedStyle* oldStyle) { |
| LayoutBlock::styleDidChange(diff, oldStyle); |
| |
| if (!m_innerBlock) |
| createInnerBlock(); |
| |
| m_buttonText->setStyle(mutableStyle()); |
| adjustInnerStyle(); |
| updateInnerBlockHeight(); |
| } |
| |
| void LayoutMenuList::updateInnerBlockHeight() { |
| const SimpleFontData* fontData = style()->font().primaryFont(); |
| DCHECK(fontData); |
| m_innerBlockHeight = (fontData ? fontData->getFontMetrics().height() : 0) + |
| m_innerBlock->borderAndPaddingHeight(); |
| } |
| |
| void LayoutMenuList::updateOptionsWidth() const { |
| float maxOptionWidth = 0; |
| |
| for (const auto& option : selectElement()->optionList()) { |
| String text = option->textIndentedToRespectGroupLabel(); |
| const ComputedStyle* itemStyle = |
| option->computedStyle() ? option->computedStyle() : style(); |
| applyTextTransform(itemStyle, text, ' '); |
| // We apply SELECT's style, not OPTION's style because m_optionsWidth is |
| // used to determine intrinsic width of the menulist box. |
| TextRun textRun = constructTextRun(style()->font(), text, *style()); |
| maxOptionWidth = std::max(maxOptionWidth, style()->font().width(textRun)); |
| } |
| m_optionsWidth = static_cast<int>(ceilf(maxOptionWidth)); |
| } |
| |
| void LayoutMenuList::updateFromElement() { |
| HTMLSelectElement* select = selectElement(); |
| HTMLOptionElement* option = select->optionToBeShown(); |
| String text = emptyString(); |
| m_optionStyle.clear(); |
| |
| if (select->isMultiple()) { |
| unsigned selectedCount = 0; |
| HTMLOptionElement* selectedOptionElement = nullptr; |
| for (const auto& option : select->optionList()) { |
| if (option->selected()) { |
| if (++selectedCount == 1) |
| selectedOptionElement = option; |
| } |
| } |
| |
| if (selectedCount == 1) { |
| text = selectedOptionElement->textIndentedToRespectGroupLabel(); |
| m_optionStyle = selectedOptionElement->mutableComputedStyle(); |
| } else { |
| Locale& locale = select->locale(); |
| String localizedNumberString = |
| locale.convertToLocalizedNumber(String::number(selectedCount)); |
| text = locale.queryString(WebLocalizedString::SelectMenuListText, |
| localizedNumberString); |
| ASSERT(!m_optionStyle); |
| } |
| } else { |
| if (option) { |
| text = option->textIndentedToRespectGroupLabel(); |
| m_optionStyle = option->mutableComputedStyle(); |
| } |
| } |
| |
| setText(text.stripWhiteSpace()); |
| |
| didUpdateActiveOption(option); |
| } |
| |
| void LayoutMenuList::setText(const String& s) { |
| if (s.isEmpty()) { |
| // FIXME: This is a hack. We need the select to have the same baseline |
| // positioning as any surrounding text. Wihtout any content, we align the |
| // bottom of the select to the bottom of the text. With content (In this |
| // case the faked " ") we correctly align the middle of the select to the |
| // middle of the text. It should be possible to remove this, just set |
| // s.impl() into the text and have things align correctly... |
| // crbug.com/485982 |
| m_isEmpty = true; |
| m_buttonText->setText(StringImpl::create(" ", 1), true); |
| } else { |
| m_isEmpty = false; |
| m_buttonText->setText(s.impl(), true); |
| } |
| adjustInnerStyle(); |
| } |
| |
| String LayoutMenuList::text() const { |
| return m_buttonText && !m_isEmpty ? m_buttonText->text() : String(); |
| } |
| |
| LayoutRect LayoutMenuList::controlClipRect( |
| const LayoutPoint& additionalOffset) const { |
| // Clip to the intersection of the content box and the content box for the |
| // inner box. This will leave room for the arrows which sit in the inner box |
| // padding, and if the inner box ever spills out of the outer box, that will |
| // get clipped too. |
| LayoutRect outerBox = contentBoxRect(); |
| outerBox.moveBy(additionalOffset); |
| |
| LayoutRect innerBox( |
| additionalOffset + m_innerBlock->location() + |
| LayoutSize(m_innerBlock->paddingLeft(), m_innerBlock->paddingTop()), |
| m_innerBlock->contentSize()); |
| |
| return intersection(outerBox, innerBox); |
| } |
| |
| void LayoutMenuList::computeIntrinsicLogicalWidths( |
| LayoutUnit& minLogicalWidth, |
| LayoutUnit& maxLogicalWidth) const { |
| updateOptionsWidth(); |
| |
| maxLogicalWidth = |
| std::max(m_optionsWidth, |
| LayoutTheme::theme().minimumMenuListSize(styleRef())) + |
| m_innerBlock->paddingLeft() + m_innerBlock->paddingRight(); |
| if (!style()->width().isPercentOrCalc()) |
| minLogicalWidth = maxLogicalWidth; |
| else |
| minLogicalWidth = LayoutUnit(); |
| } |
| |
| void LayoutMenuList::computeLogicalHeight( |
| LayoutUnit logicalHeight, |
| LayoutUnit logicalTop, |
| LogicalExtentComputedValues& computedValues) const { |
| if (style()->hasAppearance()) |
| logicalHeight = m_innerBlockHeight + borderAndPaddingHeight(); |
| LayoutBox::computeLogicalHeight(logicalHeight, logicalTop, computedValues); |
| } |
| |
| void LayoutMenuList::didSelectOption(HTMLOptionElement* option) { |
| didUpdateActiveOption(option); |
| } |
| |
| void LayoutMenuList::didUpdateActiveOption(HTMLOptionElement* option) { |
| if (!document().existingAXObjectCache()) |
| return; |
| |
| int optionIndex = option ? option->index() : -1; |
| if (m_lastActiveIndex == optionIndex) |
| return; |
| m_lastActiveIndex = optionIndex; |
| |
| if (optionIndex < 0) |
| return; |
| |
| // We skip sending accessiblity notifications for the very first option, |
| // otherwise we get extra focus and select events that are undesired. |
| if (!m_hasUpdatedActiveOption) { |
| m_hasUpdatedActiveOption = true; |
| return; |
| } |
| |
| document().existingAXObjectCache()->handleUpdateActiveMenuOption(this, |
| optionIndex); |
| } |
| |
| LayoutUnit LayoutMenuList::clientPaddingLeft() const { |
| return paddingLeft() + m_innerBlock->paddingLeft(); |
| } |
| |
| LayoutUnit LayoutMenuList::clientPaddingRight() const { |
| return paddingRight() + m_innerBlock->paddingRight(); |
| } |
| |
| } // namespace blink |