blob: d858338f13c134e16f94cccf3dde29ebef208d28 [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/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() == 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() {
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