| // Copyright 2015 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 "core/inspector/LayoutEditor.h" |
| |
| #include "bindings/core/v8/ScriptController.h" |
| #include "core/css/CSSComputedStyleDeclaration.h" |
| #include "core/css/CSSImportRule.h" |
| #include "core/css/CSSMediaRule.h" |
| #include "core/css/CSSStyleRule.h" |
| #include "core/css/CSSStyleSheet.h" |
| #include "core/css/MediaList.h" |
| #include "core/dom/NodeComputedStyle.h" |
| #include "core/dom/StaticNodeList.h" |
| #include "core/frame/FrameView.h" |
| #include "core/inspector/InspectorCSSAgent.h" |
| #include "core/inspector/InspectorDOMAgent.h" |
| #include "core/inspector/InspectorHighlight.h" |
| #include "core/inspector/protocol/Protocol.h" |
| #include "core/style/ComputedStyle.h" |
| #include "platform/ScriptForbiddenScope.h" |
| |
| namespace blink { |
| |
| namespace { |
| |
| std::unique_ptr<protocol::DictionaryValue> createAnchor( |
| const String& type, |
| const String& propertyName, |
| std::unique_ptr<protocol::DictionaryValue> valueDescription) { |
| std::unique_ptr<protocol::DictionaryValue> object = |
| protocol::DictionaryValue::create(); |
| object->setString("type", type); |
| object->setString("propertyName", propertyName); |
| object->setObject("propertyValue", std::move(valueDescription)); |
| return object; |
| } |
| |
| std::unique_ptr<protocol::DictionaryValue> pointToJSON(FloatPoint point) { |
| std::unique_ptr<protocol::DictionaryValue> object = |
| protocol::DictionaryValue::create(); |
| object->setDouble("x", point.x()); |
| object->setDouble("y", point.y()); |
| return object; |
| } |
| |
| std::unique_ptr<protocol::DictionaryValue> quadToJSON(FloatQuad& quad) { |
| std::unique_ptr<protocol::DictionaryValue> object = |
| protocol::DictionaryValue::create(); |
| object->setObject("p1", pointToJSON(quad.p1())); |
| object->setObject("p2", pointToJSON(quad.p2())); |
| object->setObject("p3", pointToJSON(quad.p3())); |
| object->setObject("p4", pointToJSON(quad.p4())); |
| return object; |
| } |
| |
| bool isMutableUnitType(CSSPrimitiveValue::UnitType unitType) { |
| return unitType == CSSPrimitiveValue::UnitType::Pixels || |
| unitType == CSSPrimitiveValue::UnitType::Ems || |
| unitType == CSSPrimitiveValue::UnitType::Percentage || |
| unitType == CSSPrimitiveValue::UnitType::Rems; |
| } |
| |
| String truncateZeroes(const String& number) { |
| if (!number.contains('.')) |
| return number; |
| |
| int removeCount = 0; |
| while (number[number.length() - removeCount - 1] == '0') |
| removeCount++; |
| |
| if (number[number.length() - removeCount - 1] == '.') |
| removeCount++; |
| |
| return number.left(number.length() - removeCount); |
| } |
| |
| InspectorHighlightConfig affectedNodesHighlightConfig() { |
| // TODO: find a better color |
| InspectorHighlightConfig config; |
| config.content = Color(95, 127, 162, 100); |
| config.padding = Color(95, 127, 162, 100); |
| config.margin = Color(95, 127, 162, 100); |
| return config; |
| } |
| |
| void collectMediaQueriesFromRule(CSSRule* rule, Vector<String>& mediaArray) { |
| MediaList* mediaList; |
| if (rule->type() == CSSRule::kMediaRule) { |
| CSSMediaRule* mediaRule = toCSSMediaRule(rule); |
| mediaList = mediaRule->media(); |
| } else if (rule->type() == CSSRule::kImportRule) { |
| CSSImportRule* importRule = toCSSImportRule(rule); |
| mediaList = importRule->media(); |
| } else { |
| mediaList = nullptr; |
| } |
| |
| if (mediaList && mediaList->length()) |
| mediaArray.append(mediaList->mediaText()); |
| } |
| |
| void buildMediaListChain(CSSRule* rule, Vector<String>& mediaArray) { |
| while (rule) { |
| collectMediaQueriesFromRule(rule, mediaArray); |
| if (rule->parentRule()) { |
| rule = rule->parentRule(); |
| } else if (rule->parentStyleSheet()) { |
| CSSStyleSheet* styleSheet = rule->parentStyleSheet(); |
| MediaList* mediaList = styleSheet->media(); |
| if (mediaList && mediaList->length()) |
| mediaArray.append(mediaList->mediaText()); |
| |
| rule = styleSheet->ownerRule(); |
| } else { |
| break; |
| } |
| } |
| } |
| |
| float roundValue(float value, CSSPrimitiveValue::UnitType unitType) { |
| float roundTo = unitType == CSSPrimitiveValue::UnitType::Pixels ? 1 : 0.05; |
| return round(value / roundTo) * roundTo; |
| } |
| |
| } // namespace |
| |
| LayoutEditor::LayoutEditor(Element* element, |
| InspectorCSSAgent* cssAgent, |
| InspectorDOMAgent* domAgent, |
| ScriptController* scriptController) |
| : m_element(element), |
| m_cssAgent(cssAgent), |
| m_domAgent(domAgent), |
| m_scriptController(scriptController), |
| m_changingProperty(CSSPropertyInvalid), |
| m_propertyInitialValue(0), |
| m_isDirty(false), |
| m_matchedStyles(cssAgent->matchingStyles(element)), |
| m_currentRuleIndex(0) {} |
| |
| LayoutEditor::~LayoutEditor() {} |
| |
| void LayoutEditor::dispose() { |
| if (!m_isDirty) |
| return; |
| |
| ErrorString errorString; |
| m_domAgent->undo(&errorString); |
| } |
| |
| DEFINE_TRACE(LayoutEditor) { |
| visitor->trace(m_element); |
| visitor->trace(m_cssAgent); |
| visitor->trace(m_domAgent); |
| visitor->trace(m_scriptController); |
| visitor->trace(m_matchedStyles); |
| } |
| |
| void LayoutEditor::rebuild() { |
| std::unique_ptr<protocol::DictionaryValue> object = |
| protocol::DictionaryValue::create(); |
| std::unique_ptr<protocol::ListValue> anchors = protocol::ListValue::create(); |
| |
| appendAnchorFor(anchors.get(), "padding", "padding-top"); |
| appendAnchorFor(anchors.get(), "padding", "padding-right"); |
| appendAnchorFor(anchors.get(), "padding", "padding-bottom"); |
| appendAnchorFor(anchors.get(), "padding", "padding-left"); |
| |
| appendAnchorFor(anchors.get(), "margin", "margin-top"); |
| appendAnchorFor(anchors.get(), "margin", "margin-right"); |
| appendAnchorFor(anchors.get(), "margin", "margin-bottom"); |
| appendAnchorFor(anchors.get(), "margin", "margin-left"); |
| |
| object->setArray("anchors", std::move(anchors)); |
| |
| FloatQuad content, padding, border, margin; |
| InspectorHighlight::buildNodeQuads(m_element.get(), &content, &padding, |
| &border, &margin); |
| object->setObject("contentQuad", quadToJSON(content)); |
| object->setObject("paddingQuad", quadToJSON(padding)); |
| object->setObject("marginQuad", quadToJSON(margin)); |
| object->setObject("borderQuad", quadToJSON(border)); |
| evaluateInOverlay("showLayoutEditor", std::move(object)); |
| editableSelectorUpdated(false); |
| } |
| |
| const CSSPrimitiveValue* LayoutEditor::getPropertyCSSValue( |
| CSSPropertyID property) const { |
| CSSStyleDeclaration* style = |
| m_cssAgent->findEffectiveDeclaration(property, m_matchedStyles); |
| if (!style) |
| return nullptr; |
| |
| const CSSValue* cssValue = style->getPropertyCSSValueInternal(property); |
| if (!cssValue || !cssValue->isPrimitiveValue()) |
| return nullptr; |
| |
| return toCSSPrimitiveValue(cssValue); |
| } |
| |
| bool LayoutEditor::growInside(String propertyName, |
| const CSSPrimitiveValue* value) { |
| FloatQuad content1, padding1, border1, margin1; |
| InspectorHighlight::buildNodeQuads(m_element.get(), &content1, &padding1, |
| &border1, &margin1); |
| |
| CSSStyleDeclaration* elementStyle = m_element->style(); |
| if (!elementStyle) |
| return false; |
| |
| String initialValue = elementStyle->getPropertyValue(propertyName); |
| String initialPriority = elementStyle->getPropertyPriority(propertyName); |
| String newValue; |
| if (value) |
| newValue = |
| String::format("%f", value->getFloatValue() + 1) + |
| CSSPrimitiveValue::unitTypeToString(value->typeWithCalcResolved()); |
| else |
| newValue = "5px"; |
| |
| TrackExceptionState exceptionState; |
| elementStyle->setProperty(propertyName, newValue, "important", |
| exceptionState); |
| m_element->ownerDocument()->updateStyleAndLayout(); |
| |
| FloatQuad content2, padding2, border2, margin2; |
| InspectorHighlight::buildNodeQuads(m_element.get(), &content2, &padding2, |
| &border2, &margin2); |
| |
| elementStyle->setProperty(propertyName, initialValue, initialPriority, |
| exceptionState); |
| m_element->ownerDocument()->updateStyleAndLayout(); |
| |
| float eps = 0.0001; |
| FloatRect boundingBox1, boundingBox2; |
| |
| if (propertyName.startsWith("padding")) { |
| boundingBox1 = padding1.boundingBox(); |
| boundingBox2 = padding2.boundingBox(); |
| } else { |
| boundingBox1 = margin1.boundingBox(); |
| boundingBox2 = margin2.boundingBox(); |
| } |
| |
| if (propertyName.endsWith("left")) |
| return std::abs(boundingBox1.x() - boundingBox2.x()) < eps; |
| |
| if (propertyName.endsWith("right")) |
| return std::abs(boundingBox1.maxX() - boundingBox2.maxX()) < eps; |
| |
| if (propertyName.endsWith("top")) |
| return std::abs(boundingBox1.y() - boundingBox2.y()) < eps; |
| |
| if (propertyName.endsWith("bottom")) |
| return std::abs(boundingBox1.maxY() - boundingBox2.maxY()) < eps; |
| return false; |
| } |
| |
| std::unique_ptr<protocol::DictionaryValue> LayoutEditor::createValueDescription( |
| const String& propertyName) { |
| const CSSPrimitiveValue* cssValue = |
| getPropertyCSSValue(cssPropertyID(propertyName)); |
| if (cssValue && !(cssValue->isLength() || cssValue->isPercentage())) |
| return nullptr; |
| |
| std::unique_ptr<protocol::DictionaryValue> object = |
| protocol::DictionaryValue::create(); |
| object->setDouble("value", cssValue ? cssValue->getFloatValue() : 0); |
| CSSPrimitiveValue::UnitType unitType = |
| cssValue ? cssValue->typeWithCalcResolved() |
| : CSSPrimitiveValue::UnitType::Pixels; |
| object->setString("unit", CSSPrimitiveValue::unitTypeToString(unitType)); |
| object->setBoolean("mutable", isMutableUnitType(unitType)); |
| |
| if (!m_growsInside.contains(propertyName)) |
| m_growsInside.set(propertyName, growInside(propertyName, cssValue)); |
| |
| object->setBoolean("growInside", m_growsInside.get(propertyName)); |
| return object; |
| } |
| |
| void LayoutEditor::appendAnchorFor(protocol::ListValue* anchors, |
| const String& type, |
| const String& propertyName) { |
| std::unique_ptr<protocol::DictionaryValue> description = |
| createValueDescription(propertyName); |
| if (description) |
| anchors->pushValue( |
| createAnchor(type, propertyName, std::move(description))); |
| } |
| |
| void LayoutEditor::overlayStartedPropertyChange(const String& anchorName) { |
| m_changingProperty = cssPropertyID(anchorName); |
| if (!m_changingProperty) |
| return; |
| |
| const CSSPrimitiveValue* cssValue = getPropertyCSSValue(m_changingProperty); |
| m_valueUnitType = cssValue ? cssValue->typeWithCalcResolved() |
| : CSSPrimitiveValue::UnitType::Pixels; |
| if (!isMutableUnitType(m_valueUnitType)) |
| return; |
| |
| switch (m_valueUnitType) { |
| case CSSPrimitiveValue::UnitType::Pixels: |
| m_factor = 1; |
| break; |
| case CSSPrimitiveValue::UnitType::Ems: |
| m_factor = m_element->computedStyle()->computedFontSize(); |
| break; |
| case CSSPrimitiveValue::UnitType::Percentage: |
| // It is hard to correctly support percentages, so we decided hack it this |
| // way: 100% = 1000px |
| m_factor = 10; |
| break; |
| case CSSPrimitiveValue::UnitType::Rems: |
| m_factor = m_element->document().computedStyle()->computedFontSize(); |
| break; |
| default: |
| ASSERT_NOT_REACHED(); |
| break; |
| } |
| m_propertyInitialValue = cssValue ? cssValue->getFloatValue() : 0; |
| } |
| |
| void LayoutEditor::overlayPropertyChanged(float cssDelta) { |
| if (m_changingProperty && m_factor) { |
| float newValue = cssDelta / m_factor + m_propertyInitialValue; |
| newValue = newValue >= 0 ? roundValue(newValue, m_valueUnitType) : 0; |
| m_isDirty |= setCSSPropertyValueInCurrentRule( |
| truncateZeroes(String::format("%.2f", newValue)) + |
| CSSPrimitiveValue::unitTypeToString(m_valueUnitType)); |
| } |
| } |
| |
| void LayoutEditor::overlayEndedPropertyChange() { |
| m_changingProperty = CSSPropertyInvalid; |
| m_propertyInitialValue = 0; |
| m_factor = 0; |
| m_valueUnitType = CSSPrimitiveValue::UnitType::Unknown; |
| } |
| |
| void LayoutEditor::commitChanges() { |
| if (!m_isDirty) |
| return; |
| |
| m_isDirty = false; |
| ErrorString errorString; |
| m_domAgent->markUndoableState(&errorString); |
| } |
| |
| void LayoutEditor::nextSelector() { |
| if (m_currentRuleIndex == m_matchedStyles.size() - 1) |
| return; |
| |
| ++m_currentRuleIndex; |
| editableSelectorUpdated(true); |
| } |
| |
| void LayoutEditor::previousSelector() { |
| if (m_currentRuleIndex == 0) |
| return; |
| |
| --m_currentRuleIndex; |
| editableSelectorUpdated(true); |
| } |
| |
| void LayoutEditor::editableSelectorUpdated(bool hasChanged) const { |
| CSSStyleDeclaration* style = m_matchedStyles.at(m_currentRuleIndex).get(); |
| evaluateInOverlay("setSelectorInLayoutEditor", currentSelectorInfo(style)); |
| if (hasChanged) |
| m_cssAgent->layoutEditorItemSelected(m_element.get(), style); |
| } |
| |
| std::unique_ptr<protocol::DictionaryValue> LayoutEditor::currentSelectorInfo( |
| CSSStyleDeclaration* style) const { |
| std::unique_ptr<protocol::DictionaryValue> object = |
| protocol::DictionaryValue::create(); |
| CSSStyleRule* rule = |
| style->parentRule() ? toCSSStyleRule(style->parentRule()) : nullptr; |
| String currentSelectorText = rule ? rule->selectorText() : "element.style"; |
| object->setString("selector", currentSelectorText); |
| |
| Document* ownerDocument = m_element->ownerDocument(); |
| if (!ownerDocument->isActive() || !rule) |
| return object; |
| |
| Vector<String> medias; |
| buildMediaListChain(rule, medias); |
| std::unique_ptr<protocol::ListValue> mediaListValue = |
| protocol::ListValue::create(); |
| for (size_t i = 0; i < medias.size(); ++i) |
| mediaListValue->pushValue(protocol::StringValue::create(medias[i])); |
| |
| object->setArray("medias", std::move(mediaListValue)); |
| |
| TrackExceptionState exceptionState; |
| StaticElementList* elements = ownerDocument->querySelectorAll( |
| AtomicString(currentSelectorText), exceptionState); |
| |
| if (!elements || exceptionState.hadException()) |
| return object; |
| |
| std::unique_ptr<protocol::ListValue> highlights = |
| protocol::ListValue::create(); |
| InspectorHighlightConfig config = affectedNodesHighlightConfig(); |
| for (unsigned i = 0; i < elements->length(); ++i) { |
| Element* element = elements->item(i); |
| if (element == m_element) |
| continue; |
| |
| InspectorHighlight highlight(element, config, false); |
| highlights->pushValue(highlight.asProtocolValue()); |
| } |
| |
| object->setArray("nodes", std::move(highlights)); |
| return object; |
| } |
| |
| bool LayoutEditor::setCSSPropertyValueInCurrentRule(const String& value) { |
| ErrorString errorString; |
| m_cssAgent->setLayoutEditorValue(&errorString, m_element.get(), |
| m_matchedStyles.at(m_currentRuleIndex), |
| m_changingProperty, value, false); |
| return errorString.isEmpty(); |
| } |
| |
| void LayoutEditor::evaluateInOverlay( |
| const String& method, |
| std::unique_ptr<protocol::Value> argument) const { |
| ScriptForbiddenScope::AllowUserAgentScript allowScript; |
| std::unique_ptr<protocol::ListValue> command = protocol::ListValue::create(); |
| command->pushValue(protocol::StringValue::create(method)); |
| command->pushValue(std::move(argument)); |
| m_scriptController->executeScriptInMainWorld( |
| "dispatch(" + command->toJSONString() + ")", |
| ScriptController::ExecuteScriptWhenScriptsDisabled); |
| } |
| |
| } // namespace blink |