|  | // Copyright 2016 The Chromium Authors | 
|  | // Use of this source code is governed by a BSD-style license that can be | 
|  | // found in the LICENSE file. | 
|  |  | 
|  | #include "components/ui_devtools/css_agent.h" | 
|  |  | 
|  | #include "base/strings/string_number_conversions.h" | 
|  | #include "base/strings/string_split.h" | 
|  | #include "base/strings/string_util.h" | 
|  | #include "base/strings/to_string.h" | 
|  | #include "components/ui_devtools/agent_util.h" | 
|  | #include "components/ui_devtools/ui_element.h" | 
|  |  | 
|  | namespace ui_devtools { | 
|  |  | 
|  | namespace CSS = protocol::CSS; | 
|  | using protocol::Array; | 
|  | using protocol::Response; | 
|  |  | 
|  | namespace { | 
|  |  | 
|  | const char kHeight[] = "height"; | 
|  | const char kWidth[] = "width"; | 
|  | const char kX[] = "x"; | 
|  | const char kY[] = "y"; | 
|  | const char kVisibility[] = "visibility"; | 
|  |  | 
|  | std::unique_ptr<CSS::SourceRange> BuildDefaultPropertySourceRange() { | 
|  | // These tell the frontend where in the stylesheet a certain style | 
|  | // is located. Since we don't have stylesheets, this is all 0. | 
|  | // We need this because CSS fields are not editable unless | 
|  | // the range is provided. | 
|  | return CSS::SourceRange::create() | 
|  | .setStartLine(0) | 
|  | .setEndLine(0) | 
|  | .setStartColumn(0) | 
|  | .setEndColumn(0) | 
|  | .build(); | 
|  | } | 
|  |  | 
|  | std::unique_ptr<CSS::SourceRange> BuildDefaultSelectorSourceRange() { | 
|  | // This is a different source range from BuildDefaultPropertySourceRange() | 
|  | // used for the Selectors, so the frontend correctly handles property edits. | 
|  | return CSS::SourceRange::create() | 
|  | .setStartLine(1) | 
|  | .setEndLine(0) | 
|  | .setStartColumn(0) | 
|  | .setEndColumn(0) | 
|  | .build(); | 
|  | } | 
|  |  | 
|  | std::unique_ptr<Array<int>> BuildDefaultMatchingSelectors() { | 
|  | auto matching_selectors = std::make_unique<Array<int>>(); | 
|  |  | 
|  | // Add index 0 to matching selectors array, so frontend uses the class name | 
|  | // from the selectors array as the header for the properties section | 
|  | matching_selectors->emplace_back(0); | 
|  | return matching_selectors; | 
|  | } | 
|  |  | 
|  | std::unique_ptr<CSS::CSSProperty> BuildCSSProperty(const std::string& name, | 
|  | const std::string& value) { | 
|  | return CSS::CSSProperty::create() | 
|  | .setRange(BuildDefaultPropertySourceRange()) | 
|  | .setName(name) | 
|  | .setValue(value) | 
|  | .build(); | 
|  | } | 
|  |  | 
|  | std::unique_ptr<Array<CSS::CSSProperty>> BuildCSSProperties( | 
|  | const std::vector<UIElement::UIProperty>& properties_vector) { | 
|  | auto css_properties = std::make_unique<Array<CSS::CSSProperty>>(); | 
|  | for (const auto& property : properties_vector) { | 
|  | css_properties->emplace_back( | 
|  | BuildCSSProperty(property.name_, property.value_)); | 
|  | } | 
|  | return css_properties; | 
|  | } | 
|  |  | 
|  | std::unique_ptr<CSS::CSSStyle> BuildCSSStyle( | 
|  | std::string stylesheet_uid, | 
|  | const std::vector<UIElement::UIProperty>& properties) { | 
|  | return CSS::CSSStyle::create() | 
|  | .setRange(BuildDefaultPropertySourceRange()) | 
|  | .setCssProperties(BuildCSSProperties(properties)) | 
|  | .setShorthandEntries(std::make_unique<Array<CSS::ShorthandEntry>>()) | 
|  | .setStyleSheetId(stylesheet_uid) | 
|  | .build(); | 
|  | } | 
|  |  | 
|  | std::unique_ptr<Array<CSS::Value>> BuildSelectors(const std::string& name) { | 
|  | auto selectors = std::make_unique<Array<CSS::Value>>(); | 
|  | selectors->emplace_back(CSS::Value::create() | 
|  | .setText(name) | 
|  | .setRange(BuildDefaultSelectorSourceRange()) | 
|  | .build()); | 
|  | return selectors; | 
|  | } | 
|  |  | 
|  | std::unique_ptr<CSS::SelectorList> BuildSelectorList(const std::string& name) { | 
|  | return CSS::SelectorList::create().setSelectors(BuildSelectors(name)).build(); | 
|  | } | 
|  |  | 
|  | std::unique_ptr<CSS::CSSRule> BuildCSSRule( | 
|  | std::string stylesheet_uid, | 
|  | const UIElement::ClassProperties& class_properties) { | 
|  | return CSS::CSSRule::create() | 
|  | .setStyleSheetId(stylesheet_uid) | 
|  | .setSelectorList(BuildSelectorList(class_properties.class_name_)) | 
|  | .setStyle(BuildCSSStyle(stylesheet_uid, class_properties.properties_)) | 
|  | .build(); | 
|  | } | 
|  |  | 
|  | std::vector<UIElement::ClassProperties> GetClassPropertiesWithBounds( | 
|  | UIElement* ui_element) { | 
|  | std::vector<UIElement::ClassProperties> properties_vector = | 
|  | ui_element->GetCustomPropertiesForMatchedStyle(); | 
|  |  | 
|  | // If GetCustomPropertiesForMatchedStyle not overridden to return custom | 
|  | // properties, populate vector with bounds properties. | 
|  | if (properties_vector.empty()) { | 
|  | gfx::Rect bounds; | 
|  | ui_element->GetBounds(&bounds); | 
|  | std::vector<UIElement::UIProperty> bound_properties; | 
|  | bound_properties.emplace_back(kX, base::NumberToString(bounds.x())); | 
|  | bound_properties.emplace_back(kY, base::NumberToString(bounds.y())); | 
|  | bound_properties.emplace_back(kWidth, base::NumberToString(bounds.width())); | 
|  | bound_properties.emplace_back(kHeight, | 
|  | base::NumberToString(bounds.height())); | 
|  | if (ui_element->type() != VIEW) { | 
|  | bool visible; | 
|  | ui_element->GetVisible(&visible); | 
|  | bound_properties.emplace_back(kVisibility, base::ToString(visible)); | 
|  | } | 
|  | properties_vector.emplace_back(ui_element->GetTypeName(), bound_properties); | 
|  | } | 
|  |  | 
|  | // Set base stylesheet ID to the last index in the vector, so when bounds | 
|  | // properties are modified, CSSAgent can update and return the right | 
|  | // properties section | 
|  | ui_element->SetBaseStylesheetId(properties_vector.size() - 1); | 
|  | return properties_vector; | 
|  | } | 
|  |  | 
|  | std::string BuildStylesheetUId(int node_id, int stylesheet_id) { | 
|  | return base::NumberToString(node_id) + "_" + | 
|  | base::NumberToString(stylesheet_id); | 
|  | } | 
|  |  | 
|  | Response NodeNotFoundError(int node_id) { | 
|  | return Response::ServerError("Node with id=" + base::NumberToString(node_id) + | 
|  | " not found"); | 
|  | } | 
|  |  | 
|  | Response ParseProperties(const std::string& style_text, | 
|  | gfx::Rect* bounds, | 
|  | bool* visible) { | 
|  | std::vector<std::string> tokens = base::SplitString( | 
|  | style_text, ":;", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY); | 
|  |  | 
|  | if (tokens.size() < 2 || tokens.size() % 2 != 0) | 
|  | return Response::ServerError("Need both a property name and value."); | 
|  |  | 
|  | for (size_t i = 0; i < tokens.size() - 1; i += 2) { | 
|  | const std::string& property = tokens.at(i); | 
|  | int value; | 
|  | if (!base::StringToInt(tokens.at(i + 1), &value)) { | 
|  | return Response::ServerError("Unable to parse value for property=" + | 
|  | property); | 
|  | } | 
|  |  | 
|  | if (property == kHeight) | 
|  | bounds->set_height(std::max(0, value)); | 
|  | else if (property == kWidth) | 
|  | bounds->set_width(std::max(0, value)); | 
|  | else if (property == kX) | 
|  | bounds->set_x(value); | 
|  | else if (property == kY) | 
|  | bounds->set_y(value); | 
|  | else if (property == kVisibility) | 
|  | *visible = std::max(0, value) == 1; | 
|  | else | 
|  | return Response::ServerError("Unsupported property=" + property); | 
|  | } | 
|  | return Response::Success(); | 
|  | } | 
|  |  | 
|  | std::unique_ptr<CSS::CSSStyleSheetHeader> BuildObjectForStyleSheetInfo( | 
|  | std::string stylesheet_uid, | 
|  | std::string url_path, | 
|  | int line) { | 
|  | std::unique_ptr<CSS::CSSStyleSheetHeader> result = | 
|  | CSS::CSSStyleSheetHeader::create() | 
|  | .setStyleSheetId(stylesheet_uid) | 
|  | .setSourceURL(kChromiumCodeSearchSrcURL + url_path + | 
|  | "?l=" + base::NumberToString(line)) | 
|  | .setStartLine(line) | 
|  | .setStartColumn(0) | 
|  | .build(); | 
|  | return result; | 
|  | } | 
|  |  | 
|  | }  // namespace | 
|  |  | 
|  | CSSAgent::CSSAgent(DOMAgent* dom_agent) : dom_agent_(dom_agent) { | 
|  | DCHECK(dom_agent_); | 
|  | } | 
|  |  | 
|  | CSSAgent::~CSSAgent() { | 
|  | disable(); | 
|  | } | 
|  |  | 
|  | Response CSSAgent::enable() { | 
|  | dom_agent_->AddObserver(this); | 
|  | return Response::Success(); | 
|  | } | 
|  |  | 
|  | Response CSSAgent::disable() { | 
|  | dom_agent_->RemoveObserver(this); | 
|  | return Response::Success(); | 
|  | } | 
|  |  | 
|  | Response CSSAgent::getMatchedStylesForNode( | 
|  | int node_id, | 
|  | std::unique_ptr<Array<CSS::RuleMatch>>* matched_css_rules) { | 
|  | UIElement* ui_element = dom_agent_->GetElementFromNodeId(node_id); | 
|  | if (!ui_element) | 
|  | return NodeNotFoundError(node_id); | 
|  | *matched_css_rules = BuildMatchedStyles(ui_element); | 
|  | return Response::Success(); | 
|  | } | 
|  |  | 
|  | Response CSSAgent::getStyleSheetText(const protocol::String& style_sheet_id, | 
|  | protocol::String* result) { | 
|  | int node_id; | 
|  | int stylesheet_id; | 
|  | std::vector<std::string> ids = base::SplitString( | 
|  | style_sheet_id, "_", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY); | 
|  | if (ids.size() < 2 || !base::StringToInt(ids[0], &node_id) || | 
|  | !base::StringToInt(ids[1], &stylesheet_id)) | 
|  | return Response::ServerError("Invalid stylesheet id"); | 
|  |  | 
|  | UIElement* ui_element = dom_agent_->GetElementFromNodeId(node_id); | 
|  | if (!ui_element) | 
|  | return Response::ServerError("Node id not found"); | 
|  |  | 
|  | auto sources = ui_element->GetSources(); | 
|  | if (static_cast<int>(sources.size()) <= stylesheet_id || stylesheet_id < 0) { | 
|  | return Response::ServerError("Stylesheet id not found"); | 
|  | } | 
|  |  | 
|  | if (GetSourceCode(sources[stylesheet_id].path_, result)) | 
|  | return Response::Success(); | 
|  | return Response::ServerError("Could not read source file"); | 
|  | } | 
|  |  | 
|  | Response CSSAgent::setStyleTexts( | 
|  | std::unique_ptr<Array<CSS::StyleDeclarationEdit>> edits, | 
|  | std::unique_ptr<Array<CSS::CSSStyle>>* result) { | 
|  | auto updated_styles = std::make_unique<Array<CSS::CSSStyle>>(); | 
|  | for (const auto& edit : *edits) { | 
|  | int node_id; | 
|  | int stylesheet_id; | 
|  |  | 
|  | std::vector<std::string> ids = | 
|  | base::SplitString(edit->getStyleSheetId(), "_", base::TRIM_WHITESPACE, | 
|  | base::SPLIT_WANT_NONEMPTY); | 
|  | if (ids.size() < 2 || !base::StringToInt(ids[0], &node_id) || | 
|  | !base::StringToInt(ids[1], &stylesheet_id)) | 
|  | return Response::ServerError("Invalid stylesheet id"); | 
|  |  | 
|  | UIElement* ui_element = dom_agent_->GetElementFromNodeId(node_id); | 
|  |  | 
|  | if (!ui_element) | 
|  | return Response::ServerError("Node id not found"); | 
|  | // Handle setting properties from metadata for elements which use metadata. | 
|  | if (!ui_element->SetPropertiesFromString(edit->getText())) { | 
|  | gfx::Rect updated_bounds; | 
|  | bool visible = false; | 
|  | if (!GetPropertiesForUIElement(ui_element, &updated_bounds, &visible)) | 
|  | return NodeNotFoundError(node_id); | 
|  |  | 
|  | Response response( | 
|  | ParseProperties(edit->getText(), &updated_bounds, &visible)); | 
|  | if (!response.IsSuccess()) | 
|  | return response; | 
|  |  | 
|  | if (!SetPropertiesForUIElement(ui_element, updated_bounds, visible)) | 
|  | return NodeNotFoundError(node_id); | 
|  | } | 
|  |  | 
|  | updated_styles->emplace_back(BuildCSSStyle( | 
|  | edit->getStyleSheetId(), GetClassPropertiesWithBounds(ui_element) | 
|  | .at(stylesheet_id) | 
|  | .properties_)); | 
|  | } | 
|  | *result = std::move(updated_styles); | 
|  | return Response::Success(); | 
|  | } | 
|  |  | 
|  | void CSSAgent::OnElementBoundsChanged(UIElement* ui_element) { | 
|  | InvalidateStyleSheet(ui_element); | 
|  | } | 
|  |  | 
|  | void CSSAgent::InvalidateStyleSheet(UIElement* ui_element) { | 
|  | // The stylesheetId for each node is equivalent to a string of its | 
|  | // node_id + "_" + index of CSS::RuleMatch in vector. | 
|  | frontend()->styleSheetChanged(BuildStylesheetUId( | 
|  | ui_element->node_id(), ui_element->GetBaseStylesheetId())); | 
|  | } | 
|  |  | 
|  | bool CSSAgent::GetPropertiesForUIElement(UIElement* ui_element, | 
|  | gfx::Rect* bounds, | 
|  | bool* visible) { | 
|  | if (ui_element) { | 
|  | ui_element->GetBounds(bounds); | 
|  | if (ui_element->type() != VIEW) | 
|  | ui_element->GetVisible(visible); | 
|  | return true; | 
|  | } | 
|  | return false; | 
|  | } | 
|  |  | 
|  | bool CSSAgent::SetPropertiesForUIElement(UIElement* ui_element, | 
|  | const gfx::Rect& bounds, | 
|  | bool visible) { | 
|  | if (ui_element) { | 
|  | ui_element->SetBounds(bounds); | 
|  | ui_element->SetVisible(visible); | 
|  | return true; | 
|  | } | 
|  | return false; | 
|  | } | 
|  |  | 
|  | std::unique_ptr<Array<CSS::RuleMatch>> CSSAgent::BuildMatchedStyles( | 
|  | UIElement* ui_element) { | 
|  | auto result = std::make_unique<Array<CSS::RuleMatch>>(); | 
|  | std::vector<UIElement::ClassProperties> properties_vector = | 
|  | GetClassPropertiesWithBounds(ui_element); | 
|  |  | 
|  | for (size_t i = 0; i < properties_vector.size(); i++) { | 
|  | result->emplace_back( | 
|  | CSS::RuleMatch::create() | 
|  | .setRule(BuildCSSRule(BuildStylesheetUId(ui_element->node_id(), i), | 
|  | properties_vector[i])) | 
|  | .setMatchingSelectors(BuildDefaultMatchingSelectors()) | 
|  | .build()); | 
|  | } | 
|  | if (!ui_element->header_sent()) { | 
|  | InitStylesheetHeaders(ui_element); | 
|  | } | 
|  | return result; | 
|  | } | 
|  |  | 
|  | void CSSAgent::InitStylesheetHeaders(UIElement* ui_element) { | 
|  | std::vector<UIElement::Source> sources = ui_element->GetSources(); | 
|  | for (size_t i = 0; i < sources.size(); i++) { | 
|  | frontend()->styleSheetAdded(BuildObjectForStyleSheetInfo( | 
|  | BuildStylesheetUId(ui_element->node_id(), i), sources[i].path_, | 
|  | sources[i].line_)); | 
|  | } | 
|  | ui_element->set_header_sent(); | 
|  | } | 
|  |  | 
|  | }  // namespace ui_devtools |