| /* |
| * 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. |
| * 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 "config.h" |
| #include "core/layout/LayoutMenuList.h" |
| |
| #include "core/HTMLNames.h" |
| #include "core/css/CSSFontSelector.h" |
| #include "core/css/resolver/StyleResolver.h" |
| #include "core/dom/AXObjectCache.h" |
| #include "core/dom/NodeComputedStyle.h" |
| #include "core/html/HTMLOptGroupElement.h" |
| #include "core/html/HTMLOptionElement.h" |
| #include "core/html/HTMLSelectElement.h" |
| #include "core/layout/LayoutBR.h" |
| #include "core/layout/LayoutScrollbar.h" |
| #include "core/layout/LayoutTheme.h" |
| #include "core/layout/LayoutView.h" |
| #include "platform/fonts/FontCache.h" |
| #include "platform/geometry/IntSize.h" |
| #include "platform/text/PlatformLocale.h" |
| #include <math.h> |
| |
| namespace blink { |
| |
| using namespace HTMLNames; |
| |
| LayoutMenuList::LayoutMenuList(Element* element) |
| : LayoutFlexibleBox(element) |
| , m_buttonText(nullptr) |
| , m_innerBlock(nullptr) |
| , m_optionsChanged(true) |
| , m_isEmpty(false) |
| , m_hasUpdatedActiveOption(false) |
| , 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); |
| } |
| |
| innerStyle.setPaddingLeft(Length(LayoutTheme::theme().popupInternalPaddingLeft(styleRef()), Fixed)); |
| innerStyle.setPaddingRight(Length(LayoutTheme::theme().popupInternalPaddingRight(styleRef()), Fixed)); |
| 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->unicodeBidi() != innerStyle.unicodeBidi())) |
| m_innerBlock->setNeedsLayoutAndPrefWidthsRecalcAndFullPaintInvalidation(LayoutInvalidationReason::StyleChange); |
| innerStyle.setTextAlign(style()->isLeftToRightDirection() ? LEFT : RIGHT); |
| innerStyle.setDirection(m_optionStyle->direction()); |
| innerStyle.setUnicodeBidi(m_optionStyle->unicodeBidi()); |
| } |
| } |
| |
| 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(); |
| |
| bool fontChanged = !oldStyle || oldStyle->font() != style()->font(); |
| if (fontChanged) |
| updateOptionsWidth(); |
| } |
| |
| void LayoutMenuList::updateOptionsWidth() |
| { |
| float maxOptionWidth = 0; |
| const WillBeHeapVector<RawPtrWillBeMember<HTMLElement>>& listItems = selectElement()->listItems(); |
| int size = listItems.size(); |
| |
| for (int i = 0; i < size; ++i) { |
| HTMLElement* element = listItems[i]; |
| if (!isHTMLOptionElement(*element)) |
| continue; |
| |
| String text = toHTMLOptionElement(element)->textIndentedToRespectGroupLabel(); |
| applyTextTransform(style(), text, ' '); |
| if (LayoutTheme::theme().popupOptionSupportsTextIndent()) { |
| // Add in the option's text indent. We can't calculate percentage values for now. |
| float optionWidth = 0; |
| if (const ComputedStyle* optionStyle = element->computedStyle()) |
| optionWidth += minimumValueForLength(optionStyle->textIndent(), 0); |
| if (!text.isEmpty()) |
| optionWidth += style()->font().width(text); |
| maxOptionWidth = std::max(maxOptionWidth, optionWidth); |
| } else if (!text.isEmpty()) { |
| maxOptionWidth = std::max(maxOptionWidth, style()->font().width(text)); |
| } |
| } |
| |
| int width = static_cast<int>(ceilf(maxOptionWidth)); |
| if (m_optionsWidth == width) |
| return; |
| |
| m_optionsWidth = width; |
| if (parent()) |
| setNeedsLayoutAndPrefWidthsRecalcAndFullPaintInvalidation(LayoutInvalidationReason::MenuWidthChanged); |
| } |
| |
| void LayoutMenuList::updateFromElement() |
| { |
| if (m_optionsChanged) { |
| updateOptionsWidth(); |
| m_optionsChanged = false; |
| } |
| |
| updateText(); |
| } |
| |
| void LayoutMenuList::updateText() |
| { |
| setTextFromOption(selectElement()->optionIndexToBeShown()); |
| } |
| |
| void LayoutMenuList::setTextFromOption(int optionIndex) |
| { |
| HTMLSelectElement* select = selectElement(); |
| const WillBeHeapVector<RawPtrWillBeMember<HTMLElement>>& listItems = select->listItems(); |
| const int size = listItems.size(); |
| |
| String text = emptyString(); |
| m_optionStyle.clear(); |
| |
| if (selectElement()->multiple()) { |
| unsigned selectedCount = 0; |
| int firstSelectedIndex = -1; |
| for (int i = 0; i < size; ++i) { |
| Element* element = listItems[i]; |
| if (!isHTMLOptionElement(*element)) |
| continue; |
| |
| if (toHTMLOptionElement(element)->selected()) { |
| if (++selectedCount == 1) |
| firstSelectedIndex = i; |
| } |
| } |
| |
| if (selectedCount == 1) { |
| ASSERT(0 <= firstSelectedIndex); |
| ASSERT(firstSelectedIndex < size); |
| HTMLOptionElement* selectedOptionElement = toHTMLOptionElement(listItems[firstSelectedIndex]); |
| ASSERT(selectedOptionElement->selected()); |
| 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 { |
| const int i = select->optionToListIndex(optionIndex); |
| if (i >= 0 && i < size) { |
| Element* element = listItems[i]; |
| if (isHTMLOptionElement(*element)) { |
| text = toHTMLOptionElement(element)->textIndentedToRespectGroupLabel(); |
| m_optionStyle = element->mutableComputedStyle(); |
| } |
| } |
| } |
| |
| setText(text.stripWhiteSpace()); |
| |
| didUpdateActiveOption(optionIndex); |
| } |
| |
| 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 |
| { |
| maxLogicalWidth = std::max(m_optionsWidth, LayoutTheme::theme().minimumMenuListSize(styleRef())) + m_innerBlock->paddingLeft() + m_innerBlock->paddingRight(); |
| if (!style()->width().hasPercent()) |
| minLogicalWidth = maxLogicalWidth; |
| } |
| |
| void LayoutMenuList::didSetSelectedIndex(int listIndex) |
| { |
| didUpdateActiveOption(selectElement()->listToOptionIndex(listIndex)); |
| } |
| |
| void LayoutMenuList::didUpdateActiveOption(int optionIndex) |
| { |
| if (!document().existingAXObjectCache()) |
| return; |
| |
| if (m_lastActiveIndex == optionIndex) |
| return; |
| m_lastActiveIndex = optionIndex; |
| |
| HTMLSelectElement* select = selectElement(); |
| int listIndex = select->optionToListIndex(optionIndex); |
| if (listIndex < 0 || listIndex >= static_cast<int>(select->listItems().size())) |
| 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(); |
| } |
| |
| const int endOfLinePadding = 2; |
| LayoutUnit LayoutMenuList::clientPaddingRight() const |
| { |
| if (style()->appearance() == MenulistPart || style()->appearance() == MenulistButtonPart) { |
| // For these appearance values, the theme applies padding to leave room for the |
| // drop-down button. But leaving room for the button inside the popup menu itself |
| // looks strange, so we return a small default padding to avoid having a large empty |
| // space appear on the side of the popup menu. |
| return endOfLinePadding; |
| } |
| |
| // If the appearance isn't MenulistPart, then the select is styled (non-native), so |
| // we want to return the user specified padding. |
| return paddingRight() + m_innerBlock->paddingRight(); |
| } |
| |
| } // namespace blink |