blob: a584d9815c0103c7abfecf3c380f052d99f36f3a [file] [log] [blame]
// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ui/views/controls/styled_label.h"
#include <stddef.h>
#include <algorithm>
#include <limits>
#include <optional>
#include <utility>
#include <variant>
#include "base/functional/callback_helpers.h"
#include "base/i18n/rtl.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/string_util.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/color/color_id.h"
#include "ui/color/color_provider.h"
#include "ui/events/event.h"
#include "ui/gfx/font_list.h"
#include "ui/gfx/text_constants.h"
#include "ui/gfx/text_elider.h"
#include "ui/gfx/text_utils.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/link.h"
#include "ui/views/controls/link_fragment.h"
#include "ui/views/style/typography.h"
#include "ui/views/style/typography_provider.h"
#include "ui/views/view_class_properties.h"
#include "ui/views/view_utils.h"
namespace views {
DEFINE_UI_CLASS_PROPERTY_KEY(bool, kStyledLabelCustomViewKey, false)
StyledLabel::RangeStyleInfo::RangeStyleInfo() = default;
StyledLabel::RangeStyleInfo::RangeStyleInfo(const RangeStyleInfo&) = default;
StyledLabel::RangeStyleInfo& StyledLabel::RangeStyleInfo::operator=(
const RangeStyleInfo&) = default;
StyledLabel::RangeStyleInfo::~RangeStyleInfo() = default;
// static
StyledLabel::RangeStyleInfo StyledLabel::RangeStyleInfo::CreateForLink(
base::RepeatingClosure callback) {
// Adapt this closure to a Link::ClickedCallback by discarding the extra arg.
return CreateForLink(base::IgnoreArgs<const ui::Event&>(std::move(callback)));
}
// static
StyledLabel::RangeStyleInfo StyledLabel::RangeStyleInfo::CreateForLink(
Link::ClickedCallback callback) {
RangeStyleInfo result;
result.callback = std::move(callback);
result.text_style = style::STYLE_LINK;
return result;
}
StyledLabel::LayoutSizeInfo::LayoutSizeInfo(int max_valid_width)
: max_valid_width(max_valid_width) {}
StyledLabel::LayoutSizeInfo::LayoutSizeInfo(const LayoutSizeInfo&) = default;
StyledLabel::LayoutSizeInfo& StyledLabel::LayoutSizeInfo::operator=(
const LayoutSizeInfo&) = default;
StyledLabel::LayoutSizeInfo::~LayoutSizeInfo() = default;
auto StyledLabel::StyleRange::operator<=>(
const StyledLabel::StyleRange& other) const {
return range <=> other.range;
}
struct StyledLabel::LayoutViews {
// All views to be added as children, line by line.
std::vector<std::vector<raw_ptr<View, VectorExperimental>>> views_per_line;
// The subset of |views| that are created by StyledLabel itself. Basically,
// this is all non-custom views; These appear in the same order as |views|.
std::vector<std::unique_ptr<View>> owned_views;
};
StyledLabel::StyledLabel() {
GetViewAccessibility().SetRole(text_context_ == style::CONTEXT_DIALOG_TITLE
? ax::mojom::Role::kTitleBar
: ax::mojom::Role::kStaticText);
}
StyledLabel::~StyledLabel() = default;
const std::u16string& StyledLabel::GetText() const {
return text_;
}
void StyledLabel::SetText(std::u16string text) {
// Failing to trim trailing whitespace will cause later confusion when the
// text elider tries to do so internally. There's no obvious reason to
// preserve trailing whitespace anyway.
base::TrimWhitespace(std::move(text), base::TRIM_TRAILING, &text);
if (text_ == text) {
return;
}
text_ = text;
GetViewAccessibility().SetName(text_);
style_ranges_.clear();
RemoveOrDeleteAllChildViews();
OnPropertyChanged(&text_, kPropertyEffectsPreferredSizeChanged);
}
gfx::FontList StyledLabel::GetFontList(const RangeStyleInfo& style_info) const {
return style_info.custom_font.value_or(TypographyProvider::Get().GetFont(
text_context_, style_info.text_style.value_or(default_text_style_)));
}
void StyledLabel::AddStyleRange(const gfx::Range& range,
const RangeStyleInfo& style_info) {
DCHECK(!range.is_reversed());
DCHECK(!range.is_empty());
DCHECK(gfx::Range(0, text_.size()).Contains(range));
// Insert the new range in sorted order.
StyleRanges new_range;
new_range.emplace_front(range, style_info);
style_ranges_.merge(new_range);
PreferredSizeChanged();
}
void StyledLabel::AddCustomView(std::unique_ptr<View> custom_view) {
DCHECK(!custom_view->owned_by_client());
custom_view->SetProperty(kStyledLabelCustomViewKey, true);
custom_views_.push_back(std::move(custom_view));
}
int StyledLabel::GetTextContext() const {
return text_context_;
}
void StyledLabel::SetTextContext(int text_context) {
if (text_context_ == text_context) {
return;
}
text_context_ = text_context;
GetViewAccessibility().SetRole(text_context_ == style::CONTEXT_DIALOG_TITLE
? ax::mojom::Role::kTitleBar
: ax::mojom::Role::kStaticText);
OnPropertyChanged(&text_context_, kPropertyEffectsPreferredSizeChanged);
}
int StyledLabel::GetDefaultTextStyle() const {
return default_text_style_;
}
void StyledLabel::SetDefaultTextStyle(int text_style) {
if (default_text_style_ == text_style) {
return;
}
default_text_style_ = text_style;
OnPropertyChanged(&default_text_style_, kPropertyEffectsPreferredSizeChanged);
}
std::optional<ui::ColorId> StyledLabel::GetDefaultEnabledColorId() const {
return default_enabled_color_id_;
}
void StyledLabel::SetDefaultEnabledColorId(
std::optional<ui::ColorId> enabled_color_id) {
if (default_enabled_color_id_ == enabled_color_id) {
return;
}
default_enabled_color_id_ = enabled_color_id;
OnPropertyChanged(&default_enabled_color_id_, kPropertyEffectsPaint);
}
int StyledLabel::GetLineHeight() const {
return line_height_.value_or(TypographyProvider::Get().GetLineHeight(
text_context_, default_text_style_));
}
void StyledLabel::SetLineHeight(int line_height) {
if (line_height_ == line_height) {
return;
}
line_height_ = line_height;
OnPropertyChanged(&line_height_, kPropertyEffectsPreferredSizeChanged);
}
std::optional<ui::ColorVariant> StyledLabel::GetDisplayedOnBackgroundColor()
const {
return displayed_on_background_color_;
}
void StyledLabel::SetDisplayedOnBackgroundColor(ui::ColorVariant color) {
if (color == displayed_on_background_color_) {
return;
}
displayed_on_background_color_ = color;
if (GetWidget()) {
UpdateLabelBackgroundColor();
}
OnPropertyChanged(&displayed_on_background_color_, kPropertyEffectsPaint);
}
bool StyledLabel::GetAutoColorReadabilityEnabled() const {
return auto_color_readability_enabled_;
}
void StyledLabel::SetAutoColorReadabilityEnabled(bool auto_color_readability) {
if (auto_color_readability_enabled_ == auto_color_readability) {
return;
}
auto_color_readability_enabled_ = auto_color_readability;
OnPropertyChanged(&auto_color_readability_enabled_, kPropertyEffectsPaint);
}
bool StyledLabel::GetSubpixelRenderingEnabled() const {
return subpixel_rendering_enabled_;
}
void StyledLabel::SetSubpixelRenderingEnabled(bool subpixel_rendering_enabled) {
if (subpixel_rendering_enabled_ == subpixel_rendering_enabled) {
return;
}
subpixel_rendering_enabled_ = subpixel_rendering_enabled;
OnPropertyChanged(&subpixel_rendering_enabled_, kPropertyEffectsPaint);
}
const StyledLabel::LayoutSizeInfo& StyledLabel::GetLayoutSizeInfoForWidth(
int w) const {
if (auto it = layout_size_info_cache_.Get(w);
it != layout_size_info_cache_.end()) {
return it->second;
}
CalculateLayout(w);
layout_size_info_cache_.Put(w, layout_size_info_);
return layout_size_info_;
}
void StyledLabel::SizeToFit(int fixed_width) {
DCHECK_LE(0, fixed_width);
fixed_width_ = fixed_width;
gfx::Size size = CalculatePreferredSize(
SizeBounds(fixed_width_ == 0 ? SizeBound() : SizeBound(width()), {}));
size.set_width(std::max(size.width(), fixed_width));
SetSize(size);
}
base::CallbackListSubscription StyledLabel::AddTextChangedCallback(
views::PropertyChangedCallback callback) {
return AddPropertyChangedCallback(&text_, std::move(callback));
}
gfx::Size StyledLabel::GetMinimumSize() const {
// Overload it otherwise BubbleDialogDelegateViewTest.StyledLabelTitle will
// fail.
return CalculatePreferredSize(
SizeBounds(width() == 0 ? SizeBound() : SizeBound(width()), {}));
}
gfx::Size StyledLabel::CalculatePreferredSize(
const SizeBounds& available_size) const {
int width = 0;
if (fixed_width_) {
width = fixed_width_;
} else if (available_size.width().is_bounded()) {
width = available_size.width().value();
}
// Respect any existing size. If there is none, default to a single line.
return GetLayoutSizeInfoForWidth(width == 0 ? std::numeric_limits<int>::max()
: width)
.total_size;
}
void StyledLabel::OnBoundsChanged(const gfx::Rect& previous_bounds) {
if (previous_bounds.width() == width()) {
return;
}
need_recreate_child_ = true;
}
void StyledLabel::Layout(PassKey) {
if (!need_recreate_child_) {
return;
}
RecreateChildViews();
}
void StyledLabel::PreferredSizeChanged() {
need_recreate_child_ = true;
layout_size_info_ = LayoutSizeInfo(0);
layout_size_info_cache_.Clear();
layout_views_.reset();
View::PreferredSizeChanged();
}
// TODO(wutao): support gfx::ALIGN_TO_HEAD alignment.
void StyledLabel::SetHorizontalAlignment(gfx::HorizontalAlignment alignment) {
DCHECK_NE(gfx::ALIGN_TO_HEAD, alignment);
alignment = gfx::MaybeFlipForRTL(alignment);
if (horizontal_alignment_ == alignment) {
return;
}
horizontal_alignment_ = alignment;
PreferredSizeChanged();
}
void StyledLabel::ClearStyleRanges() {
style_ranges_.clear();
PreferredSizeChanged();
}
void StyledLabel::ClickFirstLinkForTesting() {
GetFirstLinkForTesting()->OnKeyPressed( // IN-TEST
ui::KeyEvent(ui::EventType::kKeyPressed, ui::VKEY_SPACE, ui::EF_NONE));
}
views::Link* StyledLabel::GetFirstLinkForTesting() {
const auto it = std::ranges::find_if(children(), &IsViewClass<LinkFragment>);
return (it == children().cend()) ? nullptr : static_cast<views::Link*>(*it);
}
int StyledLabel::StartX(int excess_space) const {
int x = GetInsets().left();
// If the element should be aligned to the leading side (left in LTR, or right
// in RTL), position it at the leading side Insets (left).
if (horizontal_alignment_ ==
(base::i18n::IsRTL() ? gfx::ALIGN_RIGHT : gfx::ALIGN_LEFT)) {
return x;
}
return x + (horizontal_alignment_ == gfx::ALIGN_CENTER ? (excess_space / 2)
: excess_space);
}
void StyledLabel::CalculateLayout(int width) const {
const gfx::Insets insets = GetInsets();
width = std::max(width, insets.width());
if (width >= layout_size_info_.total_size.width() &&
width <= layout_size_info_.max_valid_width) {
return;
}
layout_size_info_ = LayoutSizeInfo(width);
layout_views_ = std::make_unique<LayoutViews>();
const int content_width = width - insets.width();
const int line_height = GetLineHeight();
RangeStyleInfo default_style;
default_style.text_style = default_text_style_;
int max_width = 0, total_height = 0;
// Try to preserve leading whitespace on the first line.
bool can_trim_leading_whitespace = false;
StyleRanges::const_iterator current_range = style_ranges_.begin();
// A pointer to the previous link fragment if a logical link consists of
// multiple `LinkFragment` elements.
LinkFragment* previous_link_fragment = nullptr;
for (std::u16string remaining_string = text_;
content_width > 0 && !remaining_string.empty();) {
layout_size_info_.line_sizes.emplace_back(0, line_height);
auto& line_size = layout_size_info_.line_sizes.back();
layout_views_->views_per_line.emplace_back();
auto& views = layout_views_->views_per_line.back();
while (!remaining_string.empty()) {
if (views.empty() && can_trim_leading_whitespace) {
if (remaining_string.front() == '\n') {
// Wrapped to the next line on \n, remove it. Other whitespace,
// e.g. spaces to indent the next line, are preserved.
remaining_string.erase(0, 1);
} else {
// Wrapped on whitespace character or characters in the middle of the
// line - none of them are needed at the beginning of the next line.
base::TrimWhitespace(remaining_string, base::TRIM_LEADING,
&remaining_string);
}
}
gfx::Range range = gfx::Range::InvalidRange();
if (current_range != style_ranges_.end()) {
range = current_range->range;
}
const size_t position = text_.size() - remaining_string.size();
std::vector<std::u16string> substrings;
// If the current range is not a custom_view, then we use
// ElideRectangleText() to determine the line wrapping. Note: if it is a
// custom_view, then the |position| should equal range.start() because the
// custom_view is treated as one unit.
if (position != range.start() ||
(current_range != style_ranges_.end() &&
!current_range->style_info.custom_view)) {
const gfx::Rect chunk_bounds(line_size.width(), 0,
content_width - line_size.width(),
line_height);
// If the start of the remaining text is inside a styled range, the font
// style may differ from the base font. The font specified by the range
// should be used when eliding text.
gfx::FontList text_font_list =
GetFontList((position >= range.start()) ? current_range->style_info
: RangeStyleInfo());
int elide_result = gfx::ElideRectangleText(
remaining_string, text_font_list, chunk_bounds.width(),
chunk_bounds.height(), gfx::WRAP_LONG_WORDS, &substrings);
if (substrings.empty()) {
// There is no room for anything. Since wrapping is enabled, this
// should only occur if there is insufficient vertical space
// remaining. ElideRectangleText() always adds a single character,
// even if there is no room horizontally.
DCHECK_NE(0, elide_result & gfx::INSUFFICIENT_SPACE_VERTICAL);
// There's no way to continue processing; clear |remaining_string| so
// the outer loop will terminate after this iteration completes.
remaining_string.clear();
break;
}
// Views are aligned to integer coordinates, but typesetting is not.
// This means that it's possible for an ElideRectangleText on a prior
// iteration to fit a word on the current line, which does not fit after
// that word is wrapped in a View for its chunk at the end of the line.
// In most cases, this will just wrap more words on to the next line.
// However, if the remaining chunk width is insufficient for the very
// _first_ word, that word will be incorrectly split. In this case,
// start a new line instead.
bool truncated_chunk =
line_size.width() != 0 &&
(elide_result & gfx::INSUFFICIENT_SPACE_FOR_FIRST_WORD) != 0;
if (substrings[0].empty() || truncated_chunk) {
// The entire line is \n, or nothing else fits on this line. Wrap,
// unless this is the first line, in which case we strip leading
// whitespace and try again.
if ((line_size.width() != 0) ||
(layout_views_->views_per_line.size() > 1)) {
break;
}
can_trim_leading_whitespace = true;
continue;
}
}
std::u16string chunk;
View* custom_view = nullptr;
std::unique_ptr<Label> label;
if (position >= range.start()) {
const RangeStyleInfo& style_info = current_range->style_info;
if (style_info.custom_view) {
custom_view = style_info.custom_view;
// Custom views must be marked as such.
DCHECK(custom_view->GetProperty(kStyledLabelCustomViewKey));
// Do not allow wrap in custom view.
DCHECK_EQ(position, range.start());
chunk = remaining_string.substr(0, range.end() - position);
} else {
chunk = substrings[0];
}
if (custom_view && position == range.start() &&
line_size.width() != 0) {
SizeBounds chunk_size(content_width - line_size.width(), {});
int custom_view_width =
custom_view->GetPreferredSize(chunk_size).width();
if (line_size.width() + custom_view_width > content_width) {
// If the chunk should not be wrapped, try to fit it entirely on the
// next line.
break;
}
}
if (chunk.size() > range.end() - position) {
chunk = chunk.substr(0, range.end() - position);
}
if (!custom_view) {
label =
CreateLabel(chunk, style_info, range, &previous_link_fragment);
} else {
previous_link_fragment = nullptr;
}
if (position + chunk.size() >= range.end()) {
++current_range;
// Links do not connect across separate style ranges.
previous_link_fragment = nullptr;
}
} else {
chunk = substrings[0];
if (position + chunk.size() > range.start()) {
chunk = chunk.substr(0, range.start() - position);
}
// This chunk is normal text.
label =
CreateLabel(chunk, default_style, range, &previous_link_fragment);
}
View* child_view = custom_view ? custom_view : label.get();
const gfx::Size child_size = child_view->GetPreferredSize(
SizeBounds(content_width - line_size.width(), {}));
// A custom view could be wider than the available width.
line_size.SetSize(
std::min(line_size.width() + child_size.width(), content_width),
std::max(line_size.height(), child_size.height()));
views.push_back(child_view);
if (label) {
layout_views_->owned_views.push_back(std::move(label));
}
remaining_string = remaining_string.substr(chunk.size());
// If |gfx::ElideRectangleText| returned more than one substring, that
// means the whole text did not fit into remaining line width, with text
// after |susbtring[0]| spilling into next line. If whole |substring[0]|
// was added to the current line (this may not be the case if part of the
// substring has different style), proceed to the next line.
if (!custom_view && substrings.size() > 1 &&
chunk.size() == substrings[0].size()) {
break;
}
}
if (views.empty() && remaining_string.empty()) {
// Remove an empty last line.
layout_size_info_.line_sizes.pop_back();
layout_views_->views_per_line.pop_back();
} else {
max_width = std::max(max_width, line_size.width());
total_height += line_size.height();
// Trim whitespace at the start of the next line.
can_trim_leading_whitespace = true;
}
}
layout_size_info_.total_size.SetSize(max_width + insets.width(),
total_height + insets.height());
}
std::unique_ptr<Label> StyledLabel::CreateLabel(
const std::u16string& text,
const RangeStyleInfo& style_info,
const gfx::Range& range,
LinkFragment** previous_link_fragment) const {
std::unique_ptr<Label> result;
if (style_info.text_style == style::STYLE_LINK ||
style_info.text_style == style::STYLE_LINK_2 ||
style_info.text_style == style::STYLE_LINK_3 ||
style_info.text_style == style::STYLE_LINK_4 ||
style_info.text_style == style::STYLE_LINK_5) {
// Nothing should (and nothing does) use a custom font for links.
DCHECK(!style_info.custom_font);
// Note this ignores |default_text_style_|, in favor of `style::STYLE_LINK`.
auto link = std::make_unique<LinkFragment>(
text, text_context_, *style_info.text_style, *previous_link_fragment);
*previous_link_fragment = link.get();
link->SetCallback(style_info.callback);
if (!style_info.accessible_name.empty()) {
link->GetViewAccessibility().SetName(style_info.accessible_name);
}
result = std::move(link);
} else if (style_info.custom_font) {
result = std::make_unique<Label>(
text, Label::CustomFont{style_info.custom_font.value()});
} else {
result = std::make_unique<Label>(
text, text_context_,
style_info.text_style.value_or(default_text_style_));
}
if (style_info.override_color_id) {
result->SetEnabledColor(style_info.override_color_id.value());
} else if (style_info.override_color) {
result->SetEnabledColor(style_info.override_color.value());
} else if (default_enabled_color_id_) {
result->SetEnabledColor(default_enabled_color_id_.value());
}
if (!style_info.tooltip.empty()) {
result->SetCustomTooltipText(style_info.tooltip);
}
if (!style_info.accessible_name.empty()) {
result->GetViewAccessibility().SetName(style_info.accessible_name);
}
if (displayed_on_background_color_) {
result->SetBackgroundColor(*displayed_on_background_color_);
}
result->SetAutoColorReadabilityEnabled(auto_color_readability_enabled_);
result->SetSubpixelRenderingEnabled(subpixel_rendering_enabled_);
return result;
}
void StyledLabel::UpdateLabelBackgroundColor() {
for (View* child : children()) {
if (!child->GetProperty(kStyledLabelCustomViewKey) &&
displayed_on_background_color_) {
// TODO(kylixrd): Should updating the label background color even be
// allowed if there are custom views?
DCHECK(IsViewClass<Label>(child) || IsViewClass<LinkFragment>(child));
static_cast<Label*>(child)->SetBackgroundColor(
*displayed_on_background_color_);
}
}
}
void StyledLabel::RemoveOrDeleteAllChildViews() {
pending_delete_views_.clear();
while (children().size() > 0) {
std::unique_ptr<View> view = RemoveChildViewT(children()[0]);
if (view->GetProperty(kStyledLabelCustomViewKey)) {
custom_views_.push_back(std::move(view));
} else {
pending_delete_views_.push_back(std::move(view));
}
}
}
void StyledLabel::RecreateChildViews() {
need_recreate_child_ = false;
CalculateLayout(width());
// If the layout has been recalculated, add and position all views.
if (layout_views_) {
// Delete all non-custom views on removal; custom views are temporarily
// moved to |custom_views_|.
RemoveOrDeleteAllChildViews();
DCHECK_EQ(layout_size_info_.line_sizes.size(),
layout_views_->views_per_line.size());
int line_y = GetInsets().top();
auto next_owned_view = layout_views_->owned_views.begin();
for (size_t line = 0; line < layout_views_->views_per_line.size(); ++line) {
const auto& line_size = layout_size_info_.line_sizes[line];
int x = StartX(width() - line_size.width());
for (views::View* view : layout_views_->views_per_line[line]) {
gfx::Size size = view->GetPreferredSize(SizeBounds(line_size));
size.set_width(std::min(size.width(), width() - x));
// Compute the view y such that the view center y and the line center y
// match. Because of added rounding errors, this is not the same as
// doing (line_size.height() - size.height()) / 2.
const int y = line_size.height() / 2 - size.height() / 2;
view->SetBoundsRect({{x, line_y + y}, size});
x += size.width();
// Transfer ownership for any views in layout_views_->owned_views or
// custom_views_. The actual pointer is the same in both arms below.
if (view->GetProperty(kStyledLabelCustomViewKey)) {
auto custom_view = std::ranges::find(custom_views_, view,
&std::unique_ptr<View>::get);
DCHECK(custom_view != custom_views_.end());
AddChildView(std::move(*custom_view));
custom_views_.erase(custom_view);
} else {
DCHECK(next_owned_view != layout_views_->owned_views.end());
DCHECK(view == next_owned_view->get());
AddChildView(std::move(*next_owned_view));
++next_owned_view;
}
}
line_y += line_size.height();
}
DCHECK(next_owned_view == layout_views_->owned_views.end());
layout_views_.reset();
} else if (horizontal_alignment_ != gfx::ALIGN_LEFT) {
// Recompute all child X coordinates in case the width has shifted, which
// will move the children if the label is center/right-aligned. If the
// width hasn't changed, all the SetX() calls below will no-op, so this
// won't have side effects.
int line_bottom = GetInsets().top();
auto i = children().begin();
for (const auto& line_size : layout_size_info_.line_sizes) {
DCHECK(i != children().end()); // Should not have an empty trailing line.
int x = StartX(width() - line_size.width());
line_bottom += line_size.height();
for (; (i != children().end()) && ((*i)->y() < line_bottom); ++i) {
(*i)->SetX(x);
x += (*i)->GetPreferredSize(SizeBounds(line_size)).width();
}
}
DCHECK(i == children().end()); // Should not be short any lines.
}
}
BEGIN_METADATA(StyledLabel)
ADD_PROPERTY_METADATA(std::u16string, Text)
ADD_PROPERTY_METADATA(int, TextContext)
ADD_PROPERTY_METADATA(int, DefaultTextStyle)
ADD_PROPERTY_METADATA(int, LineHeight)
ADD_PROPERTY_METADATA(bool, AutoColorReadabilityEnabled)
ADD_READONLY_PROPERTY_METADATA(std::optional<ui::ColorVariant>,
DisplayedOnBackgroundColor)
ADD_PROPERTY_METADATA(std::optional<ui::ColorId>, DefaultEnabledColorId)
END_METADATA
} // namespace views