| // Copyright (c) 2014 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "web/PopupMenuImpl.h" |
| |
| #include "core/HTMLNames.h" |
| #include "core/css/CSSFontSelector.h" |
| #include "core/dom/ElementTraversal.h" |
| #include "core/dom/ExecutionContextTask.h" |
| #include "core/dom/NodeComputedStyle.h" |
| #include "core/dom/StyleEngine.h" |
| #include "core/dom/TaskRunnerHelper.h" |
| #include "core/events/ScopedEventQueue.h" |
| #include "core/frame/FrameView.h" |
| #include "core/frame/LocalFrame.h" |
| #include "core/html/HTMLHRElement.h" |
| #include "core/html/HTMLOptGroupElement.h" |
| #include "core/html/HTMLOptionElement.h" |
| #include "core/html/HTMLSelectElement.h" |
| #include "core/html/parser/HTMLParserIdioms.h" |
| #include "core/layout/LayoutTheme.h" |
| #include "core/page/PagePopup.h" |
| #include "platform/geometry/IntRect.h" |
| #include "platform/text/PlatformLocale.h" |
| #include "public/platform/Platform.h" |
| #include "public/web/WebColorChooser.h" |
| #include "web/ChromeClientImpl.h" |
| #include "web/WebViewImpl.h" |
| |
| namespace blink { |
| |
| namespace { |
| |
| const char* fontWeightToString(FontWeight weight) { |
| switch (weight) { |
| case FontWeight100: |
| return "100"; |
| case FontWeight200: |
| return "200"; |
| case FontWeight300: |
| return "300"; |
| case FontWeight400: |
| return "400"; |
| case FontWeight500: |
| return "500"; |
| case FontWeight600: |
| return "600"; |
| case FontWeight700: |
| return "700"; |
| case FontWeight800: |
| return "800"; |
| case FontWeight900: |
| return "900"; |
| } |
| NOTREACHED(); |
| return nullptr; |
| } |
| |
| // TODO crbug.com/516675 Add stretch to serialization |
| |
| const char* fontStyleToString(FontStyle style) { |
| switch (style) { |
| case FontStyleNormal: |
| return "normal"; |
| case FontStyleOblique: |
| return "oblique"; |
| case FontStyleItalic: |
| return "italic"; |
| } |
| NOTREACHED(); |
| return nullptr; |
| } |
| |
| const char* textTransformToString(ETextTransform transform) { |
| switch (transform) { |
| case ETextTransform::Capitalize: |
| return "capitalize"; |
| case ETextTransform::Uppercase: |
| return "uppercase"; |
| case ETextTransform::Lowercase: |
| return "lowercase"; |
| case ETextTransform::None: |
| return "none"; |
| } |
| NOTREACHED(); |
| return ""; |
| } |
| |
| } // anonymous namespace |
| |
| class PopupMenuCSSFontSelector : public CSSFontSelector, |
| private CSSFontSelectorClient { |
| USING_GARBAGE_COLLECTED_MIXIN(PopupMenuCSSFontSelector); |
| |
| public: |
| static PopupMenuCSSFontSelector* create(Document* document, |
| CSSFontSelector* ownerFontSelector) { |
| return new PopupMenuCSSFontSelector(document, ownerFontSelector); |
| } |
| |
| ~PopupMenuCSSFontSelector(); |
| |
| // We don't override willUseFontData() for now because the old PopupListBox |
| // only worked with fonts loaded when opening the popup. |
| PassRefPtr<FontData> getFontData(const FontDescription&, |
| const AtomicString&) override; |
| |
| DECLARE_VIRTUAL_TRACE(); |
| |
| private: |
| PopupMenuCSSFontSelector(Document*, CSSFontSelector*); |
| |
| void fontsNeedUpdate(CSSFontSelector*) override; |
| |
| Member<CSSFontSelector> m_ownerFontSelector; |
| }; |
| |
| PopupMenuCSSFontSelector::PopupMenuCSSFontSelector( |
| Document* document, |
| CSSFontSelector* ownerFontSelector) |
| : CSSFontSelector(document), m_ownerFontSelector(ownerFontSelector) { |
| m_ownerFontSelector->registerForInvalidationCallbacks(this); |
| } |
| |
| PopupMenuCSSFontSelector::~PopupMenuCSSFontSelector() {} |
| |
| PassRefPtr<FontData> PopupMenuCSSFontSelector::getFontData( |
| const FontDescription& description, |
| const AtomicString& name) { |
| return m_ownerFontSelector->getFontData(description, name); |
| } |
| |
| void PopupMenuCSSFontSelector::fontsNeedUpdate(CSSFontSelector* fontSelector) { |
| dispatchInvalidationCallbacks(); |
| } |
| |
| DEFINE_TRACE(PopupMenuCSSFontSelector) { |
| visitor->trace(m_ownerFontSelector); |
| CSSFontSelector::trace(visitor); |
| CSSFontSelectorClient::trace(visitor); |
| } |
| |
| // ---------------------------------------------------------------- |
| |
| class PopupMenuImpl::ItemIterationContext { |
| STACK_ALLOCATED(); |
| |
| public: |
| ItemIterationContext(const ComputedStyle& style, SharedBuffer* buffer) |
| : m_baseStyle(style), |
| m_backgroundColor( |
| style.visitedDependentColor(CSSPropertyBackgroundColor)), |
| m_listIndex(0), |
| m_isInGroup(false), |
| m_buffer(buffer) { |
| DCHECK(m_buffer); |
| #if OS(LINUX) |
| // On other platforms, the <option> background color is the same as the |
| // <select> background color. On Linux, that makes the <option> |
| // background color very dark, so by default, try to use a lighter |
| // background color for <option>s. |
| if (LayoutTheme::theme().systemColor(CSSValueButtonface) == |
| m_backgroundColor) |
| m_backgroundColor = LayoutTheme::theme().systemColor(CSSValueMenu); |
| #endif |
| } |
| |
| void serializeBaseStyle() { |
| DCHECK(!m_isInGroup); |
| PagePopupClient::addString("baseStyle: {", m_buffer); |
| addProperty("backgroundColor", m_backgroundColor.serialized(), m_buffer); |
| addProperty( |
| "color", |
| baseStyle().visitedDependentColor(CSSPropertyColor).serialized(), |
| m_buffer); |
| addProperty("textTransform", |
| String(textTransformToString(baseStyle().textTransform())), |
| m_buffer); |
| addProperty("fontSize", baseFont().specifiedSize(), m_buffer); |
| addProperty("fontStyle", String(fontStyleToString(baseFont().style())), |
| m_buffer); |
| addProperty("fontVariant", |
| baseFont().variantCaps() == FontDescription::SmallCaps |
| ? String("small-caps") |
| : String(), |
| m_buffer); |
| |
| PagePopupClient::addString("fontFamily: [", m_buffer); |
| for (const FontFamily* f = &baseFont().family(); f; f = f->next()) { |
| addJavaScriptString(f->family().getString(), m_buffer); |
| if (f->next()) |
| PagePopupClient::addString(",", m_buffer); |
| } |
| PagePopupClient::addString("]", m_buffer); |
| PagePopupClient::addString("},\n", m_buffer); |
| } |
| |
| Color backgroundColor() const { |
| return m_isInGroup |
| ? m_groupStyle->visitedDependentColor(CSSPropertyBackgroundColor) |
| : m_backgroundColor; |
| } |
| // Do not use baseStyle() for background-color, use backgroundColor() |
| // instead. |
| const ComputedStyle& baseStyle() { |
| return m_isInGroup ? *m_groupStyle : m_baseStyle; |
| } |
| const FontDescription& baseFont() { |
| return m_isInGroup ? m_groupStyle->getFontDescription() |
| : m_baseStyle.getFontDescription(); |
| } |
| void startGroupChildren(const ComputedStyle& groupStyle) { |
| DCHECK(!m_isInGroup); |
| PagePopupClient::addString("children: [", m_buffer); |
| m_isInGroup = true; |
| m_groupStyle = &groupStyle; |
| } |
| void finishGroupIfNecessary() { |
| if (!m_isInGroup) |
| return; |
| PagePopupClient::addString("],},\n", m_buffer); |
| m_isInGroup = false; |
| m_groupStyle = nullptr; |
| } |
| |
| const ComputedStyle& m_baseStyle; |
| Color m_backgroundColor; |
| const ComputedStyle* m_groupStyle; |
| |
| unsigned m_listIndex; |
| bool m_isInGroup; |
| SharedBuffer* m_buffer; |
| }; |
| |
| // ---------------------------------------------------------------- |
| |
| PopupMenuImpl* PopupMenuImpl::create(ChromeClientImpl* chromeClient, |
| HTMLSelectElement& ownerElement) { |
| return new PopupMenuImpl(chromeClient, ownerElement); |
| } |
| |
| PopupMenuImpl::PopupMenuImpl(ChromeClientImpl* chromeClient, |
| HTMLSelectElement& ownerElement) |
| : m_chromeClient(chromeClient), |
| m_ownerElement(ownerElement), |
| m_popup(nullptr), |
| m_needsUpdate(false) {} |
| |
| PopupMenuImpl::~PopupMenuImpl() { |
| DCHECK(!m_popup); |
| } |
| |
| DEFINE_TRACE(PopupMenuImpl) { |
| visitor->trace(m_chromeClient); |
| visitor->trace(m_ownerElement); |
| PopupMenu::trace(visitor); |
| } |
| |
| void PopupMenuImpl::writeDocument(SharedBuffer* data) { |
| HTMLSelectElement& ownerElement = *m_ownerElement; |
| IntRect anchorRectInScreen = m_chromeClient->viewportToScreen( |
| ownerElement.visibleBoundsInVisualViewport(), |
| ownerElement.document().view()); |
| |
| PagePopupClient::addString( |
| "<!DOCTYPE html><head><meta charset='UTF-8'><style>\n", data); |
| data->append(Platform::current()->loadResource("pickerCommon.css")); |
| data->append(Platform::current()->loadResource("listPicker.css")); |
| PagePopupClient::addString( |
| "</style></head><body><div id=main>Loading...</div><script>\n" |
| "window.dialogArguments = {\n", |
| data); |
| addProperty("selectedIndex", ownerElement.selectedListIndex(), data); |
| const ComputedStyle* ownerStyle = ownerElement.computedStyle(); |
| ItemIterationContext context(*ownerStyle, data); |
| context.serializeBaseStyle(); |
| PagePopupClient::addString("children: [\n", data); |
| const HeapVector<Member<HTMLElement>>& items = ownerElement.listItems(); |
| for (; context.m_listIndex < items.size(); ++context.m_listIndex) { |
| Element& child = *items[context.m_listIndex]; |
| if (!isHTMLOptGroupElement(child.parentNode())) |
| context.finishGroupIfNecessary(); |
| if (isHTMLOptionElement(child)) |
| addOption(context, toHTMLOptionElement(child)); |
| else if (isHTMLOptGroupElement(child)) |
| addOptGroup(context, toHTMLOptGroupElement(child)); |
| else if (isHTMLHRElement(child)) |
| addSeparator(context, toHTMLHRElement(child)); |
| } |
| context.finishGroupIfNecessary(); |
| PagePopupClient::addString("],\n", data); |
| |
| addProperty("anchorRectInScreen", anchorRectInScreen, data); |
| float zoom = zoomFactor(); |
| float scaleFactor = m_chromeClient->windowToViewportScalar(1.f); |
| addProperty("zoomFactor", zoom / scaleFactor, data); |
| bool isRTL = !ownerStyle->isLeftToRightDirection(); |
| addProperty("isRTL", isRTL, data); |
| addProperty("paddingStart", |
| isRTL ? ownerElement.clientPaddingRight().toDouble() / zoom |
| : ownerElement.clientPaddingLeft().toDouble() / zoom, |
| data); |
| PagePopupClient::addString("};\n", data); |
| data->append(Platform::current()->loadResource("pickerCommon.js")); |
| data->append(Platform::current()->loadResource("listPicker.js")); |
| PagePopupClient::addString("</script></body>\n", data); |
| } |
| |
| void PopupMenuImpl::addElementStyle(ItemIterationContext& context, |
| HTMLElement& element) { |
| const ComputedStyle* style = m_ownerElement->itemComputedStyle(element); |
| DCHECK(style); |
| SharedBuffer* data = context.m_buffer; |
| // TODO(tkent): We generate unnecessary "style: {\n},\n" even if no |
| // additional style. |
| PagePopupClient::addString("style: {\n", data); |
| if (style->visibility() == EVisibility::Hidden) |
| addProperty("visibility", String("hidden"), data); |
| if (style->display() == EDisplay::None) |
| addProperty("display", String("none"), data); |
| const ComputedStyle& baseStyle = context.baseStyle(); |
| if (baseStyle.direction() != style->direction()) { |
| addProperty( |
| "direction", |
| String(style->direction() == TextDirection::Rtl ? "rtl" : "ltr"), data); |
| } |
| if (isOverride(style->unicodeBidi())) |
| addProperty("unicodeBidi", String("bidi-override"), data); |
| Color foregroundColor = style->visitedDependentColor(CSSPropertyColor); |
| if (baseStyle.visitedDependentColor(CSSPropertyColor) != foregroundColor) |
| addProperty("color", foregroundColor.serialized(), data); |
| Color backgroundColor = |
| style->visitedDependentColor(CSSPropertyBackgroundColor); |
| if (context.backgroundColor() != backgroundColor && |
| backgroundColor != Color::transparent) |
| addProperty("backgroundColor", backgroundColor.serialized(), data); |
| const FontDescription& baseFont = context.baseFont(); |
| const FontDescription& fontDescription = style->font().getFontDescription(); |
| if (baseFont.computedPixelSize() != fontDescription.computedPixelSize()) { |
| // We don't use FontDescription::specifiedSize() because this element |
| // might have its own zoom level. |
| addProperty("fontSize", fontDescription.computedSize() / zoomFactor(), |
| data); |
| } |
| // Our UA stylesheet has font-weight:normal for OPTION. |
| if (FontWeightNormal != fontDescription.weight()) |
| addProperty("fontWeight", |
| String(fontWeightToString(fontDescription.weight())), data); |
| if (baseFont.family() != fontDescription.family()) { |
| PagePopupClient::addString("fontFamily: [\n", data); |
| for (const FontFamily* f = &fontDescription.family(); f; f = f->next()) { |
| addJavaScriptString(f->family().getString(), data); |
| if (f->next()) |
| PagePopupClient::addString(",\n", data); |
| } |
| PagePopupClient::addString("],\n", data); |
| } |
| if (baseFont.style() != fontDescription.style()) |
| addProperty("fontStyle", String(fontStyleToString(fontDescription.style())), |
| data); |
| |
| if (baseFont.variantCaps() != fontDescription.variantCaps() && |
| fontDescription.variantCaps() == FontDescription::SmallCaps) |
| addProperty("fontVariant", String("small-caps"), data); |
| |
| if (baseStyle.textTransform() != style->textTransform()) |
| addProperty("textTransform", |
| String(textTransformToString(style->textTransform())), data); |
| |
| PagePopupClient::addString("},\n", data); |
| } |
| |
| void PopupMenuImpl::addOption(ItemIterationContext& context, |
| HTMLOptionElement& element) { |
| SharedBuffer* data = context.m_buffer; |
| PagePopupClient::addString("{", data); |
| addProperty("label", element.displayLabel(), data); |
| addProperty("value", context.m_listIndex, data); |
| if (!element.title().isEmpty()) |
| addProperty("title", element.title(), data); |
| const AtomicString& ariaLabel = |
| element.fastGetAttribute(HTMLNames::aria_labelAttr); |
| if (!ariaLabel.isEmpty()) |
| addProperty("ariaLabel", ariaLabel, data); |
| if (element.isDisabledFormControl()) |
| addProperty("disabled", true, data); |
| addElementStyle(context, element); |
| PagePopupClient::addString("},", data); |
| } |
| |
| void PopupMenuImpl::addOptGroup(ItemIterationContext& context, |
| HTMLOptGroupElement& element) { |
| SharedBuffer* data = context.m_buffer; |
| PagePopupClient::addString("{\n", data); |
| PagePopupClient::addString("type: \"optgroup\",\n", data); |
| addProperty("label", element.groupLabelText(), data); |
| addProperty("title", element.title(), data); |
| addProperty("ariaLabel", element.fastGetAttribute(HTMLNames::aria_labelAttr), |
| data); |
| addProperty("disabled", element.isDisabledFormControl(), data); |
| addElementStyle(context, element); |
| context.startGroupChildren(*m_ownerElement->itemComputedStyle(element)); |
| // We should call ItemIterationContext::finishGroupIfNecessary() later. |
| } |
| |
| void PopupMenuImpl::addSeparator(ItemIterationContext& context, |
| HTMLHRElement& element) { |
| SharedBuffer* data = context.m_buffer; |
| PagePopupClient::addString("{\n", data); |
| PagePopupClient::addString("type: \"separator\",\n", data); |
| addProperty("title", element.title(), data); |
| addProperty("ariaLabel", element.fastGetAttribute(HTMLNames::aria_labelAttr), |
| data); |
| addProperty("disabled", element.isDisabledFormControl(), data); |
| addElementStyle(context, element); |
| PagePopupClient::addString("},\n", data); |
| } |
| |
| void PopupMenuImpl::selectFontsFromOwnerDocument(Document& document) { |
| Document& ownerDocument = ownerElement().document(); |
| document.styleEngine().setFontSelector(PopupMenuCSSFontSelector::create( |
| &document, ownerDocument.styleEngine().fontSelector())); |
| } |
| |
| void PopupMenuImpl::setValueAndClosePopup(int numValue, |
| const String& stringValue) { |
| DCHECK(m_popup); |
| DCHECK(m_ownerElement); |
| if (!stringValue.isEmpty()) { |
| bool success; |
| int listIndex = stringValue.toInt(&success); |
| DCHECK(success); |
| |
| EventQueueScope scope; |
| m_ownerElement->selectOptionByPopup(listIndex); |
| if (m_popup) |
| m_chromeClient->closePagePopup(m_popup); |
| // 'change' event is dispatched here. For compatbility with |
| // Angular 1.2, we need to dispatch a change event before |
| // mouseup/click events. |
| } else { |
| if (m_popup) |
| m_chromeClient->closePagePopup(m_popup); |
| } |
| // We dispatch events on the owner element to match the legacy behavior. |
| // Other browsers dispatch click events before and after showing the popup. |
| if (m_ownerElement) { |
| PlatformMouseEvent event; |
| Element* owner = &ownerElement(); |
| owner->dispatchMouseEvent(event, EventTypeNames::mouseup); |
| owner->dispatchMouseEvent(event, EventTypeNames::click); |
| } |
| } |
| |
| void PopupMenuImpl::setValue(const String& value) { |
| DCHECK(m_ownerElement); |
| bool success; |
| int listIndex = value.toInt(&success); |
| DCHECK(success); |
| m_ownerElement->provisionalSelectionChanged(listIndex); |
| } |
| |
| void PopupMenuImpl::didClosePopup() { |
| // Clearing m_popup first to prevent from trying to close the popup again. |
| m_popup = nullptr; |
| if (m_ownerElement) |
| m_ownerElement->popupDidHide(); |
| } |
| |
| Element& PopupMenuImpl::ownerElement() { |
| return *m_ownerElement; |
| } |
| |
| Locale& PopupMenuImpl::locale() { |
| return Locale::defaultLocale(); |
| } |
| |
| void PopupMenuImpl::closePopup() { |
| if (m_popup) |
| m_chromeClient->closePagePopup(m_popup); |
| if (m_ownerElement) |
| m_ownerElement->popupDidCancel(); |
| } |
| |
| void PopupMenuImpl::dispose() { |
| if (m_popup) |
| m_chromeClient->closePagePopup(m_popup); |
| } |
| |
| void PopupMenuImpl::show() { |
| DCHECK(!m_popup); |
| m_popup = m_chromeClient->openPagePopup(this); |
| } |
| |
| void PopupMenuImpl::hide() { |
| closePopup(); |
| } |
| |
| void PopupMenuImpl::updateFromElement(UpdateReason) { |
| if (m_needsUpdate) |
| return; |
| m_needsUpdate = true; |
| ownerElement().document().postTask( |
| TaskType::UserInteraction, BLINK_FROM_HERE, |
| createSameThreadTask(&PopupMenuImpl::update, wrapPersistent(this))); |
| } |
| |
| void PopupMenuImpl::update() { |
| if (!m_popup || !m_ownerElement) |
| return; |
| ownerElement().document().updateStyleAndLayoutTree(); |
| // disconnectClient() might have been called. |
| if (!m_ownerElement) |
| return; |
| m_needsUpdate = false; |
| |
| if (!ownerElement() |
| .document() |
| .frame() |
| ->view() |
| ->visibleContentRect() |
| .intersects(ownerElement().pixelSnappedBoundingBox())) { |
| hide(); |
| return; |
| } |
| |
| RefPtr<SharedBuffer> data = SharedBuffer::create(); |
| PagePopupClient::addString("window.updateData = {\n", data.get()); |
| PagePopupClient::addString("type: \"update\",\n", data.get()); |
| ItemIterationContext context(*m_ownerElement->computedStyle(), data.get()); |
| context.serializeBaseStyle(); |
| PagePopupClient::addString("children: [", data.get()); |
| const HeapVector<Member<HTMLElement>>& items = m_ownerElement->listItems(); |
| for (; context.m_listIndex < items.size(); ++context.m_listIndex) { |
| Element& child = *items[context.m_listIndex]; |
| if (!isHTMLOptGroupElement(child.parentNode())) |
| context.finishGroupIfNecessary(); |
| if (isHTMLOptionElement(child)) |
| addOption(context, toHTMLOptionElement(child)); |
| else if (isHTMLOptGroupElement(child)) |
| addOptGroup(context, toHTMLOptGroupElement(child)); |
| else if (isHTMLHRElement(child)) |
| addSeparator(context, toHTMLHRElement(child)); |
| } |
| context.finishGroupIfNecessary(); |
| PagePopupClient::addString("],\n", data.get()); |
| IntRect anchorRectInScreen = m_chromeClient->viewportToScreen( |
| m_ownerElement->visibleBoundsInVisualViewport(), |
| ownerElement().document().view()); |
| addProperty("anchorRectInScreen", anchorRectInScreen, data.get()); |
| PagePopupClient::addString("}\n", data.get()); |
| m_popup->postMessage(String::fromUTF8(data->data(), data->size())); |
| } |
| |
| void PopupMenuImpl::disconnectClient() { |
| m_ownerElement = nullptr; |
| // Cannot be done during finalization, so instead done when the |
| // layout object is destroyed and disconnected. |
| dispose(); |
| } |
| |
| } // namespace blink |