blob: d9e07423163396edc3d64d84061b3b5ad328ff9f [file] [log] [blame]
// 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