Commit ea7508e2 authored by Peter Boström's avatar Peter Boström Committed by Commit Bot

Use DialogModel for ExtensionUninstallDialogView

Adds the following to DialogModel:
* Support for dialog icons through DialogModel::Builder::SetIcon.
* Support for base::string16 (not message_id) in DialogModelLabel.
* Support for character break in DialogModelLabel.

Then refactors ExtensionUninstallDialogView to use DialogModel, which
required adding the above items to DialogModel.

Bug: 1106422
Change-Id: I59267cf6037f907171f9ff5121c37528b1d1a371
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2454978
Commit-Queue: Peter Boström <pbos@chromium.org>
Reviewed-by: default avatarDevlin <rdevlin.cronin@chromium.org>
Reviewed-by: default avatarScott Violet <sky@chromium.org>
Cr-Commit-Position: refs/heads/master@{#816711}
parent 6574db9c
......@@ -73,6 +73,7 @@ ExtensionUninstallDialog::ExtensionUninstallDialog(
gfx::NativeWindow parent,
ExtensionUninstallDialog::Delegate* delegate)
: profile_(profile), parent_(parent), delegate_(delegate) {
DCHECK(delegate_);
if (parent)
parent_window_tracker_ = NativeWindowTracker::Create(parent);
}
......
......@@ -4,8 +4,6 @@
#include <memory>
#include "base/compiler_specific.h"
#include "base/macros.h"
#include "base/strings/string16.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
......@@ -25,11 +23,13 @@
#include "extensions/common/constants.h"
#include "extensions/common/extension.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/models/dialog_model.h"
#include "ui/compositor/compositor.h"
#include "ui/compositor/layer.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/image/image_skia_operations.h"
#include "ui/views/bubble/bubble_dialog_delegate_view.h"
#include "ui/views/bubble/bubble_dialog_model_host.h"
#include "ui/views/controls/button/checkbox.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"
......@@ -39,7 +39,8 @@
#include "ui/views/window/dialog_delegate.h"
namespace {
class ExtensionUninstallDialogDelegateView;
constexpr int kCheckboxId = 1;
// Views implementation of the uninstall dialog.
class ExtensionUninstallDialogViews
......@@ -49,61 +50,28 @@ class ExtensionUninstallDialogViews
Profile* profile,
gfx::NativeWindow parent,
extensions::ExtensionUninstallDialog::Delegate* delegate);
ExtensionUninstallDialogViews(const ExtensionUninstallDialogViews&) = delete;
ExtensionUninstallDialogViews& operator=(
const ExtensionUninstallDialogViews&) = delete;
~ExtensionUninstallDialogViews() override;
// Called when the ExtensionUninstallDialogDelegate has been destroyed to make
// sure we invalidate pointers. This object will also be freed.
void DialogDelegateDestroyed();
// Forwards the accept and cancels to the delegate.
void DialogAccepted(bool checkbox_checked);
void DialogCanceled();
// Forwards that the dialog has been accepted to the delegate.
void DialogAccepted();
// Reports a canceled dialog to the delegate (unless accepted).
void DialogClosing();
private:
void Show() override;
ExtensionUninstallDialogDelegateView* view_ = nullptr;
DISALLOW_COPY_AND_ASSIGN(ExtensionUninstallDialogViews);
};
// The dialog's view, owned by the views framework.
class ExtensionUninstallDialogDelegateView
: public views::BubbleDialogDelegateView {
public:
// Constructor for view component of dialog. triggering_extension may be null
// if the uninstall dialog was manually triggered (from chrome://extensions).
ExtensionUninstallDialogDelegateView(
ExtensionUninstallDialogViews* dialog_view,
ToolbarActionView* anchor_view,
const extensions::Extension* extension,
const extensions::Extension* triggering_extension,
const gfx::ImageSkia* image);
~ExtensionUninstallDialogDelegateView() override;
// Called when the ExtensionUninstallDialog has been destroyed to make sure
// we invalidate pointers.
void DialogDestroyed() { dialog_ = nullptr; }
private:
// views::View:
const char* GetClassName() const override;
// views::DialogDelegateView:
gfx::Size CalculatePreferredSize() const override;
// views::WidgetDelegate:
ui::ModalType GetModalType() const override {
return is_bubble_ ? ui::MODAL_TYPE_NONE : ui::MODAL_TYPE_WINDOW;
}
ExtensionUninstallDialogViews* dialog_;
const bool is_bubble_;
views::Label* heading_;
views::Checkbox* checkbox_;
// Pointer to the DialogModel for the dialog. This is cleared when the dialog
// is being closed and OnDialogClosed is reported. As such it prevents access
// to the dialog after it's been closed, as well as preventing multiple
// reports of OnDialogClosed.
ui::DialogModel* dialog_model_ = nullptr;
DISALLOW_COPY_AND_ASSIGN(ExtensionUninstallDialogDelegateView);
// WeakPtrs because the associated dialog may outlive |this|, which is owned
// by the caller of extensions::ExtensionsUninstallDialog::Create().
base::WeakPtrFactory<ExtensionUninstallDialogViews> weak_ptr_factory_{this};
};
ExtensionUninstallDialogViews::ExtensionUninstallDialogViews(
......@@ -113,14 +81,52 @@ ExtensionUninstallDialogViews::ExtensionUninstallDialogViews(
: extensions::ExtensionUninstallDialog(profile, parent, delegate) {}
ExtensionUninstallDialogViews::~ExtensionUninstallDialogViews() {
// Close the widget (the views framework will delete view_).
if (view_) {
view_->DialogDestroyed();
view_->GetWidget()->CloseNow();
}
if (dialog_model_)
dialog_model_->host()->Close();
DCHECK(!dialog_model_);
}
void ExtensionUninstallDialogViews::Show() {
// TODO(pbos): Consider separating dialog model from views code.
ui::DialogModel::Builder dialog_builder;
dialog_builder
.SetTitle(
l10n_util::GetStringFUTF16(IDS_EXTENSION_PROMPT_UNINSTALL_TITLE,
base::UTF8ToUTF16(extension()->name())))
.OverrideShowCloseButton(false)
.SetWindowClosingCallback(
base::BindOnce(&ExtensionUninstallDialogViews::DialogClosing,
weak_ptr_factory_.GetWeakPtr()))
.SetIcon(ui::ImageModel::FromImageSkia(
gfx::ImageSkiaOperations::CreateResizedImage(
icon(), skia::ImageOperations::ResizeMethod::RESIZE_GOOD,
gfx::Size(extension_misc::EXTENSION_ICON_SMALL,
extension_misc::EXTENSION_ICON_SMALL))))
.AddOkButton(
base::BindOnce(&ExtensionUninstallDialogViews::DialogAccepted,
weak_ptr_factory_.GetWeakPtr()),
l10n_util::GetStringUTF16(IDS_EXTENSION_PROMPT_UNINSTALL_BUTTON))
.AddCancelButton(
base::OnceClosure() /* Cancel is covered by WindowClosingCallback */);
if (triggering_extension()) {
dialog_builder.AddBodyText(
ui::DialogModelLabel(
l10n_util::GetStringFUTF16(
IDS_EXTENSION_PROMPT_UNINSTALL_TRIGGERED_BY_EXTENSION,
base::UTF8ToUTF16(triggering_extension()->name())))
.set_is_secondary()
.set_allow_character_break());
}
if (ShouldShowCheckbox()) {
dialog_builder.AddCheckbox(kCheckboxId,
ui::DialogModelLabel(GetCheckboxLabel()));
}
std::unique_ptr<ui::DialogModel> dialog_model = dialog_builder.Build();
dialog_model_ = dialog_model.get();
BrowserView* const browser_view =
parent() ? BrowserView::GetBrowserViewForNativeWindow(parent()) : nullptr;
ToolbarActionView* anchor_view = nullptr;
......@@ -141,156 +147,50 @@ void ExtensionUninstallDialogViews::Show() {
if (reference_view && reference_view->GetVisible())
anchor_view = reference_view;
}
view_ = new ExtensionUninstallDialogDelegateView(
this, anchor_view, extension(), triggering_extension(), &icon());
if (anchor_view) {
auto bubble = std::make_unique<views::BubbleDialogModelHost>(
std::move(dialog_model), anchor_view, views::BubbleBorder::TOP_RIGHT);
if (container) {
container->ShowWidgetForExtension(
views::BubbleDialogDelegateView::CreateBubble(view_),
views::BubbleDialogDelegateView::CreateBubble(std::move(bubble)),
extension()->id());
} else {
DCHECK(!base::FeatureList::IsEnabled(features::kExtensionsToolbarMenu));
views::BubbleDialogDelegateView::CreateBubble(view_)->Show();
views::BubbleDialogDelegateView::CreateBubble(std::move(bubble))->Show();
}
} else {
constrained_window::CreateBrowserModalDialogViews(view_, parent())->Show();
}
}
void ExtensionUninstallDialogViews::DialogDelegateDestroyed() {
// Checks view_ to ensure OnDialogClosed() will not be called twice.
if (view_) {
view_ = nullptr;
OnDialogClosed(CLOSE_ACTION_CANCELED);
// TODO(pbos): Add unique_ptr version of CreateBrowserModalDialogViews and
// remove .release().
constrained_window::CreateBrowserModalDialogViews(
views::BubbleDialogModelHost::CreateModal(std::move(dialog_model),
ui::MODAL_TYPE_WINDOW)
.release(),
parent())
->Show();
}
chrome::RecordDialogCreation(chrome::DialogIdentifier::EXTENSION_UNINSTALL);
}
void ExtensionUninstallDialogViews::DialogAccepted(bool checkbox_checked) {
// The widget gets destroyed when the dialog is accepted.
DCHECK(view_);
view_->DialogDestroyed();
view_ = nullptr;
OnDialogClosed(checkbox_checked ? CLOSE_ACTION_UNINSTALL_AND_CHECKBOX_CHECKED
void ExtensionUninstallDialogViews::DialogAccepted() {
DCHECK(dialog_model_);
const bool checkbox_is_checked =
ShouldShowCheckbox() &&
dialog_model_->GetCheckboxByUniqueId(kCheckboxId)->is_checked();
dialog_model_ = nullptr;
OnDialogClosed(checkbox_is_checked
? CLOSE_ACTION_UNINSTALL_AND_CHECKBOX_CHECKED
: CLOSE_ACTION_UNINSTALL);
}
void ExtensionUninstallDialogViews::DialogCanceled() {
// The widget gets destroyed when the dialog is canceled.
DCHECK(view_);
view_->DialogDestroyed();
view_ = nullptr;
void ExtensionUninstallDialogViews::DialogClosing() {
if (!dialog_model_)
return;
dialog_model_ = nullptr;
OnDialogClosed(CLOSE_ACTION_CANCELED);
}
ExtensionUninstallDialogDelegateView::ExtensionUninstallDialogDelegateView(
ExtensionUninstallDialogViews* dialog_view,
ToolbarActionView* anchor_view,
const extensions::Extension* extension,
const extensions::Extension* triggering_extension,
const gfx::ImageSkia* image)
: BubbleDialogDelegateView(anchor_view,
anchor_view ? views::BubbleBorder::TOP_RIGHT
: views::BubbleBorder::NONE),
dialog_(dialog_view),
is_bubble_(anchor_view != nullptr),
checkbox_(nullptr) {
SetButtonLabel(
ui::DIALOG_BUTTON_OK,
l10n_util::GetStringUTF16(IDS_EXTENSION_PROMPT_UNINSTALL_BUTTON));
SetIcon(gfx::ImageSkiaOperations::CreateResizedImage(
*image, skia::ImageOperations::ResizeMethod::RESIZE_GOOD,
gfx::Size(extension_misc::EXTENSION_ICON_SMALL,
extension_misc::EXTENSION_ICON_SMALL)));
SetShowCloseButton(false);
SetShowIcon(true);
SetTitle(l10n_util::GetStringFUTF16(IDS_EXTENSION_PROMPT_UNINSTALL_TITLE,
base::UTF8ToUTF16(extension->name())));
SetAcceptCallback(base::BindOnce(
[](ExtensionUninstallDialogDelegateView* view) {
if (view->dialog_) {
view->dialog_->DialogAccepted(view->checkbox_ &&
view->checkbox_->GetChecked());
}
},
base::Unretained(this)));
SetCancelCallback(base::BindOnce(
[](ExtensionUninstallDialogDelegateView* view) {
if (view->dialog_)
view->dialog_->DialogCanceled();
},
base::Unretained(this)));
ChromeLayoutProvider* provider = ChromeLayoutProvider::Get();
SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kVertical, gfx::Insets(),
provider->GetDistanceMetric(views::DISTANCE_RELATED_CONTROL_VERTICAL)));
// Add margins for the icon plus the icon-title padding so that the dialog
// contents align with the title text.
set_margins(
margins() +
gfx::Insets(0, margins().left() + extension_misc::EXTENSION_ICON_SMALL, 0,
0));
if (triggering_extension) {
heading_ = new views::Label(
l10n_util::GetStringFUTF16(
IDS_EXTENSION_PROMPT_UNINSTALL_TRIGGERED_BY_EXTENSION,
base::UTF8ToUTF16(triggering_extension->name())),
views::style::CONTEXT_DIALOG_BODY_TEXT, views::style::STYLE_SECONDARY);
heading_->SetMultiLine(true);
heading_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
heading_->SetAllowCharacterBreak(true);
AddChildView(heading_);
}
if (dialog_->ShouldShowCheckbox()) {
checkbox_ = new views::Checkbox(dialog_->GetCheckboxLabel());
checkbox_->SetMultiLine(true);
AddChildView(checkbox_);
}
if (anchor_view)
anchor_view->AnimateInkDrop(views::InkDropState::ACTIVATED, nullptr);
chrome::RecordDialogCreation(chrome::DialogIdentifier::EXTENSION_UNINSTALL);
}
ExtensionUninstallDialogDelegateView::~ExtensionUninstallDialogDelegateView() {
// If we're here, 2 things could have happened. Either the user closed the
// dialog nicely and one of the installed/canceled methods has been called
// (in which case dialog_ will be null), *or* neither of them have been
// called and we are being forced closed by our parent widget. In this case,
// we need to make sure to notify dialog_ not to call us again, since we're
// about to be freed by the Widget framework.
if (dialog_)
dialog_->DialogDelegateDestroyed();
// If there is still a toolbar action view its ink drop should be deactivated
// when the uninstall dialog goes away. This lookup is repeated as the dialog
// can go away during dialog's lifetime (especially when uninstalling).
views::View* anchor_view = GetAnchorView();
if (anchor_view) {
static_cast<ToolbarActionView*>(anchor_view)
->AnimateInkDrop(views::InkDropState::DEACTIVATED, nullptr);
}
}
const char* ExtensionUninstallDialogDelegateView::GetClassName() const {
return "ExtensionUninstallDialogDelegateView";
}
gfx::Size ExtensionUninstallDialogDelegateView::CalculatePreferredSize() const {
const int width =
ChromeLayoutProvider::Get()->GetDistanceMetric(
is_bubble_ ? views::DISTANCE_BUBBLE_PREFERRED_WIDTH
: views::DISTANCE_MODAL_DIALOG_PREFERRED_WIDTH) -
margins().width();
return gfx::Size(width, GetHeightForWidth(width));
}
} // namespace
// static
......
......@@ -135,7 +135,6 @@ IN_PROC_BROWSER_TEST_F(ExtensionUninstallDialogViewBrowserTest,
EXPECT_TRUE(delegate.canceled());
}
#if defined(OS_CHROMEOS)
// Test that we don't crash when uninstalling an extension from a web app
// window in Ash. Context: crbug.com/825554
IN_PROC_BROWSER_TEST_F(ExtensionUninstallDialogViewBrowserTest,
......@@ -155,12 +154,13 @@ IN_PROC_BROWSER_TEST_F(ExtensionUninstallDialogViewBrowserTest,
Browser* app_browser =
web_app::LaunchWebAppBrowser(browser()->profile(), app_id);
TestExtensionUninstallDialogDelegate delegate{base::DoNothing()};
std::unique_ptr<extensions::ExtensionUninstallDialog> dialog;
{
base::RunLoop run_loop;
dialog = extensions::ExtensionUninstallDialog::Create(
app_browser->profile(), app_browser->window()->GetNativeWindow(),
nullptr);
&delegate);
run_loop.RunUntilIdle();
}
......@@ -172,7 +172,6 @@ IN_PROC_BROWSER_TEST_F(ExtensionUninstallDialogViewBrowserTest,
run_loop.RunUntilIdle();
}
}
#endif // defined(OS_CHROMEOS)
class ParameterizedExtensionUninstallDialogViewBrowserTest
: public InProcessBrowserTest,
......
......@@ -37,6 +37,7 @@
#include "extensions/test/test_extension_dir.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "ui/views/animation/ink_drop.h"
#include "ui/views/bubble/bubble_dialog_model_host.h"
#include "ui/views/controls/button/image_button.h"
#include "ui/views/layout/animating_layout_manager.h"
#include "ui/views/layout/animating_layout_manager_test_util.h"
......@@ -88,7 +89,7 @@ class ExtensionsMenuViewBrowserTest : public ExtensionsToolbarBrowserTest {
// Trigger uninstall dialog.
views::NamedWidgetShownWaiter waiter(
views::test::AnyWidgetTestPasskey{},
"ExtensionUninstallDialogDelegateView");
views::BubbleDialogModelHost::kViewClassName);
extensions::ExtensionContextMenuModel menu_model(
extensions()[0].get(), browser(),
extensions::ExtensionContextMenuModel::PINNED, nullptr,
......
......@@ -14,6 +14,7 @@
#include "base/util/type_safety/pass_key.h"
#include "ui/base/models/dialog_model_field.h"
#include "ui/base/models/dialog_model_host.h"
#include "ui/base/models/image_model.h"
#include "ui/base/ui_base_types.h"
namespace ui {
......@@ -126,6 +127,11 @@ class COMPONENT_EXPORT(UI_BASE) DialogModel final {
return *this;
}
Builder& SetIcon(ImageModel icon) {
model_->icon_ = std::move(icon);
return *this;
}
// Make screen readers announce the contents of the dialog as it appears.
// See |ax::mojom::Role::kAlertDialog|.
Builder& SetIsAlertDialog() {
......@@ -281,6 +287,8 @@ class COMPONENT_EXPORT(UI_BASE) DialogModel final {
return title_;
}
const ImageModel& icon(util::PassKey<DialogModelHost>) const { return icon_; }
base::Optional<int> initially_focused_field(
util::PassKey<DialogModelHost>) const {
return initially_focused_field_;
......@@ -327,8 +335,8 @@ class COMPONENT_EXPORT(UI_BASE) DialogModel final {
base::Optional<bool> override_show_close_button_;
bool close_on_deactivate_ = true;
base::string16 title_;
ImageModel icon_;
static constexpr int kExtraButtonId = DIALOG_BUTTON_LAST + 1;
std::vector<std::unique_ptr<DialogModelField>> fields_;
base::Optional<int> initially_focused_field_;
bool is_alert_dialog_ = false;
......
......@@ -3,7 +3,9 @@
// found in the LICENSE file.
#include "ui/base/models/dialog_model_field.h"
#include "base/bind.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/models/dialog_model.h"
namespace ui {
......@@ -18,9 +20,23 @@ DialogModelLabel::Link::Link(int message_id, base::RepeatingClosure closure)
DialogModelLabel::Link::Link(const Link&) = default;
DialogModelLabel::Link::~Link() = default;
DialogModelLabel::DialogModelLabel(int message_id) : message_id_(message_id) {}
DialogModelLabel::DialogModelLabel(int message_id)
: message_id_(message_id),
string_(l10n_util::GetStringUTF16(message_id_)) {}
DialogModelLabel::DialogModelLabel(int message_id, std::vector<Link> links)
: message_id_(message_id), links_(std::move(links)) {}
: message_id_(message_id), links_(std::move(links)) {
// Note that this constructor does not set |string_| which is invalid for
// labels with links.
}
DialogModelLabel::DialogModelLabel(base::string16 fixed_string)
: message_id_(-1), string_(std::move(fixed_string)) {}
const base::string16& DialogModelLabel::GetString(
util::PassKey<DialogModelHost>) const {
DCHECK(links_.empty());
return string_;
}
DialogModelLabel::DialogModelLabel(const DialogModelLabel&) = default;
......
......@@ -45,6 +45,7 @@ class COMPONENT_EXPORT(UI_BASE) DialogModelLabel {
};
explicit DialogModelLabel(int message_id);
explicit DialogModelLabel(base::string16 fixed_string);
DialogModelLabel(const DialogModelLabel&);
DialogModelLabel& operator=(const DialogModelLabel&) = delete;
~DialogModelLabel();
......@@ -54,11 +55,22 @@ class COMPONENT_EXPORT(UI_BASE) DialogModelLabel {
static DialogModelLabel CreateWithLinks(int message_id,
std::vector<Link> links);
// Gets the string. Not for use with links, in which case the caller must use
// links() and message_id() to construct the final label. This is required to
// style the final label appropriately and support link callbacks. The caller
// is responsible for checking links().empty() before calling this.
const base::string16& GetString(util::PassKey<DialogModelHost>) const;
DialogModelLabel& set_is_secondary() {
is_secondary_ = true;
return *this;
}
DialogModelLabel& set_allow_character_break() {
allow_character_break_ = true;
return *this;
}
int message_id(util::PassKey<DialogModelHost>) const { return message_id_; }
const std::vector<Link> links(util::PassKey<DialogModelHost>) const {
return links_;
......@@ -66,13 +78,18 @@ class COMPONENT_EXPORT(UI_BASE) DialogModelLabel {
bool is_secondary(util::PassKey<DialogModelHost>) const {
return is_secondary_;
}
bool allow_character_break(util::PassKey<DialogModelHost>) const {
return allow_character_break_;
}
private:
explicit DialogModelLabel(int message_id, std::vector<Link> links);
const int message_id_;
const base::string16 string_;
const std::vector<Link> links_;
bool is_secondary_ = false;
bool allow_character_break_ = false;
};
// These "field" classes represent entries in a DialogModel. They are owned
......
......@@ -134,11 +134,19 @@ BubbleDialogModelHost::BubbleDialogModelHost(
SetButtons(button_mask);
SetTitle(model_->title(GetPassKey()));
if (model_->override_show_close_button(GetPassKey())) {
SetShowCloseButton(*model_->override_show_close_button(GetPassKey()));
} else {
SetShowCloseButton(!IsModalDialog());
}
if (!model_->icon(GetPassKey()).IsEmpty()) {
// TODO(pbos): Consider adding ImageModel support to SetIcon().
SetIcon(model_->icon(GetPassKey()).GetImage().AsImageSkia());
SetShowIcon(true);
}
if (model_->is_alert_dialog(GetPassKey()))
SetAccessibleRole(ax::mojom::Role::kAlertDialog);
......@@ -290,8 +298,20 @@ void BubbleDialogModelHost::AddInitialFields() {
first_row = false;
}
set_margins(LayoutProvider::Get()->GetDialogInsetsForContentType(
first_field_content_type, last_field_content_type));
gfx::Insets margins = LayoutProvider::Get()->GetDialogInsetsForContentType(
first_field_content_type, last_field_content_type);
if (!model_->icon(GetPassKey()).IsEmpty()) {
// If we have a window icon, inset margins additionally to align with
// title label.
// TODO(pbos): Reconsider this. Aligning with title gives a massive gap on
// the left side of the dialog. This style is from
// ExtensionUninstallDialogView as part of refactoring it to use
// DialogModel.
margins.set_left(
margins.left() + model_->icon(GetPassKey()).Size().width() +
LayoutProvider::Get()->GetInsetsMetric(INSETS_DIALOG_TITLE).left());
}
set_margins(margins);
}
void BubbleDialogModelHost::OnWindowClosing() {
......@@ -479,8 +499,7 @@ std::unique_ptr<View> BubbleDialogModelHost::CreateViewForLabel(
}
auto text_label = std::make_unique<Label>(
l10n_util::GetStringUTF16(dialog_label.message_id(GetPassKey())),
style::CONTEXT_DIALOG_BODY_TEXT,
dialog_label.GetString(GetPassKey()), style::CONTEXT_DIALOG_BODY_TEXT,
dialog_label.is_secondary(GetPassKey()) ? style::STYLE_SECONDARY
: style::STYLE_PRIMARY);
text_label->SetMultiLine(true);
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment