blob: 837040bd26aff30a0a27295dcea6215c6a544d27 [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 "config.h"
#include "core/inspector/LayoutEditor.h"
#include "core/css/CSSComputedStyleDeclaration.h"
#include "core/css/CSSImportRule.h"
#include "core/css/CSSMediaRule.h"
#include "core/css/CSSStyleRule.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/layout/LayoutBox.h"
#include "core/layout/LayoutInline.h"
#include "core/layout/LayoutObject.h"
#include "core/style/ComputedStyle.h"
#include "platform/JSONValues.h"
namespace blink {
namespace {
PassRefPtr<JSONObject> createAnchor(const FloatPoint& point, const String& type, const String& propertyName, FloatPoint deltaVector, PassRefPtr<JSONObject> valueDescription)
{
RefPtr<JSONObject> object = JSONObject::create();
object->setNumber("x", point.x());
object->setNumber("y", point.y());
object->setString("type", type);
object->setString("propertyName", propertyName);
RefPtr<JSONObject> deltaVectorJSON = JSONObject::create();
deltaVectorJSON->setNumber("x", deltaVector.x());
deltaVectorJSON->setNumber("y", deltaVector.y());
object->setObject("deltaVector", deltaVectorJSON.release());
object->setObject("propertyValue", valueDescription);
return object.release();
}
FloatPoint orthogonalVector(FloatPoint from, FloatPoint to, FloatPoint defaultVector)
{
if (from == to)
return defaultVector;
return FloatPoint(to.y() - from.y(), from.x() - to.x());
}
FloatPoint mean(const FloatPoint& p1, const FloatPoint& p2)
{
float x = (p1.x() + p2.x()) / 2;
float y = (p1.y() + p2.y()) / 2;
return FloatPoint(x, y);
}
FloatQuad means(const FloatQuad& quad)
{
FloatQuad result;
result.setP1(mean(quad.p1(), quad.p2()));
result.setP2(mean(quad.p2(), quad.p3()));
result.setP3(mean(quad.p3(), quad.p4()));
result.setP4(mean(quad.p4(), quad.p1()));
return result;
}
FloatQuad orthogonalVectors(const FloatQuad& quad)
{
FloatQuad result;
result.setP1(orthogonalVector(quad.p1(), quad.p2(), FloatPoint(0, -1)));
result.setP2(orthogonalVector(quad.p2(), quad.p3(), FloatPoint(1, 0)));
result.setP3(orthogonalVector(quad.p3(), quad.p4(), FloatPoint(0, 1)));
result.setP4(orthogonalVector(quad.p4(), quad.p1(), FloatPoint(-1, 0)));
return result;
}
float det(float a11, float a12, float a21, float a22)
{
return a11 * a22 - a12 * a21;
}
FloatPoint projection(const FloatPoint& anchor, const FloatPoint& orthogonalVector, const FloatPoint& point)
{
float a1 = orthogonalVector.x();
float b1 = orthogonalVector.y();
float c1 = - anchor.x() * a1 - anchor.y() * b1;
float a2 = orthogonalVector.y();
float b2 = - orthogonalVector.x();
float c2 = - point.x() * a2 - point.y() * b2;
float d = det(a1, b1, a2, b2);
ASSERT(d < 0);
float x = - det(c1, b1, c2, b2) / d;
float y = - det(a1, c1, a2, c2) / d;
return FloatPoint(x, y);
}
FloatPoint translatePoint(const FloatPoint& point, const FloatPoint& orthogonalVector, float distance)
{
float d = sqrt(orthogonalVector.x() * orthogonalVector.x() + orthogonalVector.y() * orthogonalVector.y());
float dx = - orthogonalVector.y() * distance / d;
float dy = orthogonalVector.x() * distance / d;
return FloatPoint(point.x() + dx, point.y() + dy);
}
FloatQuad translateAndProject(const FloatQuad& origin, const FloatQuad& orthogonals, const FloatQuad& projectOn, float distance)
{
FloatQuad result;
result.setP1(projection(projectOn.p1(), orthogonals.p1(), translatePoint(origin.p1(), orthogonals.p1(), distance)));
result.setP2(projection(projectOn.p2(), orthogonals.p2(), translatePoint(origin.p2(), orthogonals.p2(), distance)));
// We want to translate at top and bottom point in the same direction, so we use p1 here.
result.setP3(projection(projectOn.p3(), orthogonals.p3(), translatePoint(origin.p3(), orthogonals.p1(), distance)));
result.setP4(projection(projectOn.p4(), orthogonals.p4(), translatePoint(origin.p4(), orthogonals.p2(), distance)));
return result;
}
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);
}
float toValidValue(CSSPropertyID propertyId, float newValue)
{
if (CSSPropertyPaddingBottom <= propertyId && propertyId <= CSSPropertyPaddingTop)
return newValue >= 0 ? newValue : 0;
return newValue;
}
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::MEDIA_RULE) {
CSSMediaRule* mediaRule = toCSSMediaRule(rule);
mediaList = mediaRule->media();
} else if (rule->type() == CSSRule::IMPORT_RULE) {
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;
}
}
}
} // namespace
LayoutEditor::LayoutEditor(Element* element, InspectorCSSAgent* cssAgent, InspectorDOMAgent* domAgent)
: m_element(element)
, m_cssAgent(cssAgent)
, m_domAgent(domAgent)
, m_changingProperty(CSSPropertyInvalid)
, m_propertyInitialValue(0)
, m_isDirty(false)
, m_matchedRules(m_cssAgent->matchedRulesList(element))
, m_currentRuleIndex(m_matchedRules->length())
{
}
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_matchedRules);
}
PassRefPtr<JSONObject> LayoutEditor::buildJSONInfo() const
{
FloatQuad content, padding, border, margin;
InspectorHighlight::buildNodeQuads(m_element.get(), &content, &padding, &border, &margin);
FloatQuad orthogonals = orthogonalVectors(padding);
RefPtr<JSONObject> object = JSONObject::create();
RefPtr<JSONArray> anchors = JSONArray::create();
FloatQuad contentMeans = means(content);
FloatQuad paddingHandles = translateAndProject(contentMeans, orthogonals, padding, 0);
appendAnchorFor(anchors.get(), "padding", "padding-top", paddingHandles.p1(), orthogonals.p1());
appendAnchorFor(anchors.get(), "padding", "padding-right", paddingHandles.p2(), orthogonals.p2());
appendAnchorFor(anchors.get(), "padding", "padding-bottom", paddingHandles.p3(), orthogonals.p3());
appendAnchorFor(anchors.get(), "padding", "padding-left", paddingHandles.p4(), orthogonals.p4());
FloatQuad marginHandles = translateAndProject(contentMeans, orthogonals, margin, 12);
appendAnchorFor(anchors.get(), "margin", "margin-top", marginHandles.p1(), orthogonals.p1());
appendAnchorFor(anchors.get(), "margin", "margin-right", marginHandles.p2(), orthogonals.p2());
appendAnchorFor(anchors.get(), "margin", "margin-bottom", marginHandles.p3(), orthogonals.p3());
appendAnchorFor(anchors.get(), "margin", "margin-left", marginHandles.p4(), orthogonals.p4());
object->setArray("anchors", anchors.release());
return object.release();
}
RefPtrWillBeRawPtr<CSSPrimitiveValue> LayoutEditor::getPropertyCSSValue(CSSPropertyID property) const
{
RefPtrWillBeRawPtr<CSSStyleDeclaration> style = m_cssAgent->findEffectiveDeclaration(property, m_matchedRules.get(), m_element->style());
if (!style)
return nullptr;
RefPtrWillBeRawPtr<CSSValue> cssValue = style->getPropertyCSSValueInternal(property);
if (!cssValue || !cssValue->isPrimitiveValue())
return nullptr;
return toCSSPrimitiveValue(cssValue.get());
}
PassRefPtr<JSONObject> LayoutEditor::createValueDescription(const String& propertyName) const
{
RefPtrWillBeRawPtr<CSSPrimitiveValue> cssValue = getPropertyCSSValue(cssPropertyID(propertyName));
if (cssValue && !(cssValue->isLength() || cssValue->isPercentage()))
return nullptr;
RefPtr<JSONObject> object = JSONObject::create();
object->setNumber("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));
return object.release();
}
void LayoutEditor::appendAnchorFor(JSONArray* anchors, const String& type, const String& propertyName, const FloatPoint& position, const FloatPoint& orthogonalVector) const
{
RefPtr<JSONObject> description = createValueDescription(propertyName);
if (description)
anchors->pushObject(createAnchor(position, type, propertyName, orthogonalVector, description.release()));
}
void LayoutEditor::overlayStartedPropertyChange(const String& anchorName)
{
m_changingProperty = cssPropertyID(anchorName);
if (!m_changingProperty)
return;
RefPtrWillBeRawPtr<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 = toValidValue(m_changingProperty, cssDelta / m_factor + m_propertyInitialValue);
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);
}
bool LayoutEditor::currentStyleIsInline()
{
return m_currentRuleIndex == m_matchedRules->length();
}
void LayoutEditor::nextSelector()
{
if (m_currentRuleIndex == 0)
return;
m_currentRuleIndex--;
}
void LayoutEditor::previousSelector()
{
if (currentStyleIsInline())
return;
m_currentRuleIndex++;
}
String LayoutEditor::currentSelectorInfo()
{
RefPtr<JSONObject> object = JSONObject::create();
String currentSelectorText = currentStyleIsInline() ? "inline style" : toCSSStyleRule(m_matchedRules->item(m_currentRuleIndex))->selectorText();
object->setString("selector", currentSelectorText);
Document* ownerDocument = m_element->ownerDocument();
if (!ownerDocument->isActive() || currentStyleIsInline())
return object->toJSONString();
bool hasSameSelectors = false;
for (unsigned i = 0; i < m_matchedRules->length(); i++) {
if (i != m_currentRuleIndex && toCSSStyleRule(m_matchedRules->item(i))->selectorText() == currentSelectorText) {
hasSameSelectors = true;
break;
}
}
if (hasSameSelectors) {
Vector<String> medias;
buildMediaListChain(m_matchedRules->item(m_currentRuleIndex), medias);
RefPtr<JSONArray> mediasJSONArray = JSONArray::create();
for (size_t i = 0; i < medias.size(); ++i)
mediasJSONArray->pushString(medias[i]);
object->setArray("medias", mediasJSONArray.release());
}
TrackExceptionState exceptionState;
RefPtrWillBeRawPtr<StaticElementList> elements = ownerDocument->querySelectorAll(AtomicString(currentSelectorText), exceptionState);
if (!elements || exceptionState.hadException())
return object->toJSONString();
RefPtr<JSONArray> highlights = JSONArray::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->pushObject(highlight.asJSONObject());
}
object->setArray("nodes", highlights.release());
return object->toJSONString();
}
bool LayoutEditor::setCSSPropertyValueInCurrentRule(const String& value)
{
RefPtrWillBeRawPtr<CSSStyleDeclaration> effectiveDeclaration = m_cssAgent->findEffectiveDeclaration(m_changingProperty, m_matchedRules.get(), m_element->style());
bool forceImportant = false;
if (effectiveDeclaration) {
CSSStyleRule* effectiveRule = nullptr;
if (effectiveDeclaration->parentRule() && effectiveDeclaration->parentRule()->type() == CSSRule::STYLE_RULE)
effectiveRule = toCSSStyleRule(effectiveDeclaration->parentRule());
unsigned effectiveRuleIndex = m_matchedRules->length();
for (unsigned i = 0; i < m_matchedRules->length(); ++i) {
if (m_matchedRules->item(i) == effectiveRule) {
effectiveRuleIndex = i;
break;
}
}
forceImportant = effectiveDeclaration->getPropertyPriority(getPropertyNameString(m_changingProperty)) == "important";
forceImportant |= effectiveRuleIndex > m_currentRuleIndex;
}
CSSStyleDeclaration* style = currentStyleIsInline() ? m_element->style() : toCSSStyleRule(m_matchedRules->item(m_currentRuleIndex))->style();
String errorString;
m_cssAgent->setCSSPropertyValue(&errorString, m_element.get(), style, m_changingProperty, value, forceImportant);
return errorString.isEmpty();
}
} // namespace blink