blob: 5a2d4767a8f6aae507d89fc2fd41ccf29f7261dc [file] [log] [blame]
// 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/events/ScopedEventQueue.h"
#include "core/frame/FrameView.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 CAPITALIZE:
return "capitalize";
case UPPERCASE:
return "uppercase";
case LOWERCASE:
return "lowercase";
case TTNONE:
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() == NONE)
addProperty("display", String("none"), data);
const ComputedStyle& baseStyle = context.baseStyle();
if (baseStyle.direction() != style->direction())
addProperty("direction", String(style->direction() == 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);
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.
}
// 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()
{
if (m_popup)
m_chromeClient->closePagePopup(m_popup);
}
void PopupMenuImpl::updateFromElement(UpdateReason)
{
if (m_needsUpdate)
return;
m_needsUpdate = true;
ownerElement().document().postTask(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