Commit 879a4ed9 authored by Avi Drissman's avatar Avi Drissman Committed by Commit Bot

Add the New badge to Cocoa menus

This reverts e35a8ac4 and
provides the capability of putting [New] tags into Cocoa
context menus.

Bug: 1114257
Change-Id: I411335701f61af47a515e498f7f09c5467fd60e2
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2344944
Commit-Queue: Avi Drissman <avi@chromium.org>
Reviewed-by: default avatarDana Fried <dfried@chromium.org>
Cr-Commit-Position: refs/heads/master@{#797493}
parent dc0fc847
......@@ -199,6 +199,7 @@ RenderViewContextMenuMacCocoa::~RenderViewContextMenuMacCocoa() {
void RenderViewContextMenuMacCocoa::Show() {
menu_controller_.reset([[MenuControllerCocoa alloc] initWithModel:&menu_model_
delegate:nil
useWithPopUpButtonCell:NO]);
gfx::Point params_position(params_.x, params_.y);
......
......@@ -110,11 +110,13 @@ void StatusIconMac::CreateMenu(ui::MenuModel* model, NSString* toolTip) {
if (!toolTip) {
menu_.reset([[MenuControllerCocoa alloc] initWithModel:model
delegate:nil
useWithPopUpButtonCell:NO]);
} else {
// When using a popup button cell menu controller, an extra blank item is
// added at index 0. Use this item for the tooltip.
menu_.reset([[MenuControllerCocoa alloc] initWithModel:model
delegate:nil
useWithPopUpButtonCell:YES]);
NSMenuItem* toolTipItem = [[menu_ menu] itemAtIndex:0];
[toolTipItem setTitle:toolTip];
......
......@@ -112,9 +112,8 @@ class BrowserTabStripController::TabContextMenuContents
// Because we use "new" badging for feature promos, we cannot use system-
// native context menus. (See crbug.com/1109256.)
const int run_flags = views::MenuRunner::HAS_MNEMONICS |
views::MenuRunner::CONTEXT_MENU |
views::MenuRunner::FORCE_VIEWS;
const int run_flags =
views::MenuRunner::HAS_MNEMONICS | views::MenuRunner::CONTEXT_MENU;
menu_runner_ = std::make_unique<views::MenuRunner>(model_.get(), run_flags);
}
TabContextMenuContents(const TabContextMenuContents&) = delete;
......
......@@ -15,6 +15,13 @@ namespace ui {
class MenuModel;
}
COMPONENT_EXPORT(UI_BASE)
@protocol MenuControllerCocoaDelegate
- (void)controllerWillAddItem:(NSMenuItem*)menuItem
fromModel:(ui::MenuModel*)model
atIndex:(NSInteger)index;
@end
// A controller for the cross-platform menu model. The menu that's created
// has the tag and represented object set for each menu item. The object is a
// NSValue holding a pointer to the model for that level of the menu (to
......@@ -40,6 +47,7 @@ COMPONENT_EXPORT(UI_BASE)
// slightly different form (0th item is empty). Note this attribute of the menu
// cannot be changed after it has been created.
- (instancetype)initWithModel:(ui::MenuModel*)model
delegate:(id<MenuControllerCocoaDelegate>)delegate
useWithPopUpButtonCell:(BOOL)useWithCell;
// Programmatically close the constructed menu.
......
......@@ -107,14 +107,12 @@ bool MenuHasVisibleItems(const ui::MenuModel* model) {
- (void)itemSelected:(id)sender;
@end
@interface ResponsiveNSMenuItem : NSMenuItem
@end
@implementation MenuControllerCocoa {
base::WeakPtr<ui::MenuModel> _model;
base::scoped_nsobject<NSMenu> _menu;
BOOL _useWithPopUpButtonCell; // If YES, 0th item is blank
BOOL _isMenuOpen;
id<MenuControllerCocoaDelegate> _delegate;
}
@synthesize useWithPopUpButtonCell = _useWithPopUpButtonCell;
......@@ -133,9 +131,11 @@ bool MenuHasVisibleItems(const ui::MenuModel* model) {
}
- (instancetype)initWithModel:(ui::MenuModel*)model
delegate:(id<MenuControllerCocoaDelegate>)delegate
useWithPopUpButtonCell:(BOOL)useWithCell {
if ((self = [super init])) {
_model = model->AsWeakPtr();
_delegate = delegate;
_useWithPopUpButtonCell = useWithCell;
[self menu];
}
......@@ -153,6 +153,10 @@ bool MenuHasVisibleItems(const ui::MenuModel* model) {
[super dealloc];
}
- (void)setDelegate:(id<MenuControllerCocoaDelegate>)delegate {
_delegate = delegate;
}
- (void)cancel {
if (_isMenuOpen) {
[_menu cancelTracking];
......@@ -239,6 +243,10 @@ bool MenuHasVisibleItems(const ui::MenuModel* model) {
}
}
}
if (_delegate)
[_delegate controllerWillAddItem:item fromModel:model atIndex:index];
[menu insertItem:item atIndex:index];
}
......
......@@ -170,6 +170,7 @@ class OwningDelegate : public Delegate {
model_.AddItem(1, ASCIIToUTF16("foo"));
controller_.reset([[WatchedLifetimeMenuController alloc]
initWithModel:&model_
delegate:nil
useWithPopUpButtonCell:NO]);
[controller_ setDeallocCalled:did_dealloc];
}
......@@ -225,6 +226,7 @@ TEST_F(MenuControllerTest, EmptyMenu) {
SimpleMenuModel model(&delegate);
base::scoped_nsobject<MenuControllerCocoa> menu([[MenuControllerCocoa alloc]
initWithModel:&model
delegate:nil
useWithPopUpButtonCell:NO]);
EXPECT_EQ(0, [[menu menu] numberOfItems]);
}
......@@ -241,6 +243,7 @@ TEST_F(MenuControllerTest, BasicCreation) {
base::scoped_nsobject<MenuControllerCocoa> menu([[MenuControllerCocoa alloc]
initWithModel:&model
delegate:nil
useWithPopUpButtonCell:NO]);
EXPECT_EQ(6, [[menu menu] numberOfItems]);
......@@ -267,6 +270,7 @@ TEST_F(MenuControllerTest, Submenus) {
base::scoped_nsobject<MenuControllerCocoa> menu([[MenuControllerCocoa alloc]
initWithModel:&model
delegate:nil
useWithPopUpButtonCell:NO]);
EXPECT_EQ(3, [[menu menu] numberOfItems]);
......@@ -301,6 +305,7 @@ TEST_F(MenuControllerTest, EmptySubmenu) {
base::scoped_nsobject<MenuControllerCocoa> menu([[MenuControllerCocoa alloc]
initWithModel:&model
delegate:nil
useWithPopUpButtonCell:NO]);
EXPECT_EQ(2, [[menu menu] numberOfItems]);
......@@ -328,6 +333,7 @@ TEST_F(MenuControllerTest, EmptySubmenuWhenAllChildItemsAreHidden) {
base::scoped_nsobject<MenuControllerCocoa> menu([[MenuControllerCocoa alloc]
initWithModel:&model
delegate:nil
useWithPopUpButtonCell:NO]);
EXPECT_EQ(2, [[menu menu] numberOfItems]);
......@@ -361,6 +367,7 @@ TEST_F(MenuControllerTest, HiddenSubmenu) {
// Create the controller.
base::scoped_nsobject<MenuControllerCocoa> menu_controller(
[[MenuControllerCocoa alloc] initWithModel:&model
delegate:nil
useWithPopUpButtonCell:NO]);
EXPECT_EQ(2, [[menu_controller menu] numberOfItems]);
delegate.menu_to_close_ = [menu_controller menu];
......@@ -410,6 +417,7 @@ TEST_F(MenuControllerTest, DisabledSubmenu) {
// Create the controller.
base::scoped_nsobject<MenuControllerCocoa> menu_controller(
[[MenuControllerCocoa alloc] initWithModel:&model
delegate:nil
useWithPopUpButtonCell:NO]);
delegate.menu_to_close_ = [menu_controller menu];
......@@ -450,6 +458,7 @@ TEST_F(MenuControllerTest, PopUpButton) {
// title.
base::scoped_nsobject<MenuControllerCocoa> menu([[MenuControllerCocoa alloc]
initWithModel:&model
delegate:nil
useWithPopUpButtonCell:YES]);
EXPECT_EQ(4, [[menu menu] numberOfItems]);
EXPECT_EQ(base::string16(),
......@@ -466,6 +475,7 @@ TEST_F(MenuControllerTest, Execute) {
model.AddItem(1, ASCIIToUTF16("one"));
base::scoped_nsobject<MenuControllerCocoa> menu([[MenuControllerCocoa alloc]
initWithModel:&model
delegate:nil
useWithPopUpButtonCell:NO]);
EXPECT_EQ(1, [[menu menu] numberOfItems]);
......@@ -496,6 +506,7 @@ TEST_F(MenuControllerTest, Validate) {
base::scoped_nsobject<MenuControllerCocoa> menu([[MenuControllerCocoa alloc]
initWithModel:&model
delegate:nil
useWithPopUpButtonCell:NO]);
EXPECT_EQ(3, [[menu menu] numberOfItems]);
......@@ -514,6 +525,7 @@ TEST_F(MenuControllerTest, LabelFontList) {
base::scoped_nsobject<MenuControllerCocoa> menu([[MenuControllerCocoa alloc]
initWithModel:&model
delegate:nil
useWithPopUpButtonCell:NO]);
EXPECT_EQ(2, [[menu menu] numberOfItems]);
......@@ -556,6 +568,7 @@ TEST_F(MenuControllerTest, Dynamic) {
model.AddItem(1, ASCIIToUTF16("foo"));
base::scoped_nsobject<MenuControllerCocoa> menu([[MenuControllerCocoa alloc]
initWithModel:&model
delegate:nil
useWithPopUpButtonCell:NO]);
EXPECT_EQ(1, [[menu menu] numberOfItems]);
// Validate() simulates opening the menu - the item label/icon should be
......@@ -598,6 +611,7 @@ TEST_F(MenuControllerTest, OpenClose) {
// Create the controller.
base::scoped_nsobject<MenuControllerCocoa> menu([[MenuControllerCocoa alloc]
initWithModel:&model
delegate:nil
useWithPopUpButtonCell:NO]);
delegate.menu_to_close_ = [menu menu];
......
......@@ -208,6 +208,30 @@ struct VIEWS_EXPORT MenuConfig {
// Margins for footnotes (HIGHLIGHTED item at the end of a menu).
int footnote_vertical_margin = 11;
// New Badge -----------------------------------------------------------------
// Note that there are a few differences between Views and Mac constants here
// that are due to the fact that the rendering is different and therefore
// tweaks to the spacing need to be made to achieve the same visual result.
// Difference in the font size (in pixels) between menu label font and "new"
// badge font size.
static constexpr int kNewBadgeFontSizeAdjustment = -1;
// Space between primary text and "new" badge.
static constexpr int kNewBadgeHorizontalMargin = 8;
// Highlight padding around "new" text.
static constexpr int kNewBadgeInternalPadding = 4;
static constexpr int kNewBadgeInternalPaddingTopMac = 1;
// The baseline offset of the "new" badge image to the menu text baseline.
static constexpr int kNewBadgeBaslineOffsetMac = -4;
// The corner radius of the rounded rect for the "new" badge.
static constexpr int kNewBadgeCornerRadius = 3;
static_assert(kNewBadgeCornerRadius <= kNewBadgeInternalPadding,
"New badge corner radius should not exceed padding.");
private:
// Configures a MenuConfig as appropriate for the current platform.
void Init();
......
......@@ -53,21 +53,6 @@ namespace views {
namespace {
// Difference in the font size (in pixels) between menu label font and "new"
// badge font size.
constexpr int kNewBadgeFontSizeAdjustment = -1;
// Space between primary text and "new" badge.
constexpr int kNewBadgeHorizontalMargin = 8;
// Highlight size around "new" badge.
constexpr gfx::Insets kNewBadgeInternalPadding{4};
// The corner radius of the rounded rect for the "new" badge.
constexpr int kNewBadgeCornerRadius = 3;
static_assert(kNewBadgeCornerRadius <= kNewBadgeInternalPadding.left(),
"New badge corner radius should not exceed padding.");
// Returns the appropriate font to use for the "new" badge based on the font
// currently being used to render the title of the menu item.
gfx::FontList DeriveNewBadgeFont(const gfx::FontList& primary_font) {
......@@ -76,8 +61,8 @@ gfx::FontList DeriveNewBadgeFont(const gfx::FontList& primary_font) {
// add a small degree of bold to prevent color smearing/blurring due to font
// smoothing. This ensures readability on all platforms and in both light and
// dark modes.
return primary_font.DeriveWithSizeDelta(kNewBadgeFontSizeAdjustment)
.DeriveWithWeight(gfx::Font::Weight::MEDIUM);
return primary_font.Derive(MenuConfig::kNewBadgeFontSizeAdjustment,
gfx::Font::NORMAL, gfx::Font::Weight::MEDIUM);
}
// Returns the horizontal space required for the "new" badge.
......@@ -86,7 +71,8 @@ int GetNewBadgeRequiredWidth(const gfx::FontList& primary_font) {
l10n_util::GetStringUTF16(IDS_MENU_ITEM_NEW_BADGE);
gfx::FontList badge_font = DeriveNewBadgeFont(primary_font);
return gfx::GetStringWidth(new_text, badge_font) +
kNewBadgeInternalPadding.width() + 2 * kNewBadgeHorizontalMargin;
2 * MenuConfig::kNewBadgeInternalPadding +
2 * MenuConfig::kNewBadgeHorizontalMargin;
}
// Returns the highlight rect for the "new" badge given the font and text rect
......@@ -94,8 +80,8 @@ int GetNewBadgeRequiredWidth(const gfx::FontList& primary_font) {
gfx::Rect GetNewBadgeRectOutsetAroundText(const gfx::FontList& badge_font,
const gfx::Rect& badge_text_rect) {
gfx::Rect badge_rect = badge_text_rect;
badge_rect.Inset(
-gfx::AdjustVisualBorderForFont(badge_font, kNewBadgeInternalPadding));
badge_rect.Inset(-gfx::AdjustVisualBorderForFont(
badge_font, gfx::Insets(MenuConfig::kNewBadgeInternalPadding)));
return badge_rect;
}
......@@ -1038,7 +1024,7 @@ void MenuItemView::PaintButton(gfx::Canvas* canvas, PaintButtonMode mode) {
DrawNewBadge(
canvas,
gfx::Point(label_start + gfx::GetStringWidth(title(), style.font_list) +
kNewBadgeHorizontalMargin,
MenuConfig::kNewBadgeHorizontalMargin,
top_margin),
style.font_list, flags);
}
......@@ -1370,7 +1356,7 @@ void MenuItemView::DrawNewBadge(gfx::Canvas* canvas,
gfx::Rect badge_text_bounds(unmirrored_badge_start,
gfx::GetStringSize(new_text, badge_font));
badge_text_bounds.Offset(
kNewBadgeInternalPadding.left(),
MenuConfig::kNewBadgeInternalPadding,
gfx::GetFontCapHeightCenterOffset(primary_font, badge_font));
if (base::i18n::IsRTL())
badge_text_bounds.set_x(GetMirroredXForRect(badge_text_bounds));
......@@ -1382,7 +1368,7 @@ void MenuItemView::DrawNewBadge(gfx::Canvas* canvas,
new_flags.setColor(background_color);
canvas->DrawRoundRect(
GetNewBadgeRectOutsetAroundText(badge_font, badge_text_bounds),
kNewBadgeCornerRadius, new_flags);
MenuConfig::kNewBadgeCornerRadius, new_flags);
// Render the badge text.
const SkColor foreground_color = GetNativeTheme()->GetSystemColor(
......
......@@ -67,7 +67,6 @@ class VIEWS_EXPORT MenuRunner {
// The menu is a nested context menu. For example, click a folder on the
// bookmark bar, then right click an entry to get its context menu.
// Currently implies FORCE_VIEWS.
IS_NESTED = 1 << 1,
// Used for showing a menu during a drop operation. This does NOT block the
......@@ -109,10 +108,6 @@ class VIEWS_EXPORT MenuRunner {
// Indicates that the menu should show mnemonics.
SHOULD_SHOW_MNEMONICS = 1 << 10,
// Indicates that the menu contains Views-only features, and therefore
// should not be rendered using a native system menu.
FORCE_VIEWS = 1 << 11,
};
// Creates a new MenuRunner, which may use a native menu if available.
......
......@@ -14,6 +14,7 @@
#include "ui/views/controls/menu/menu_runner_impl_interface.h"
@class MenuControllerCocoa;
@class MenuControllerDelegate;
namespace views {
namespace test {
......@@ -45,6 +46,9 @@ class VIEWS_EXPORT MenuRunnerImplCocoa : public MenuRunnerImplInterface {
// The Cocoa menu controller that this instance is bridging.
base::scoped_nsobject<MenuControllerCocoa> menu_controller_;
// The delegate for the |menu_controller_|.
base::scoped_nsobject<MenuControllerDelegate> menu_delegate_;
// Are we in run waiting for it to return?
bool running_;
......
......@@ -4,17 +4,178 @@
#import "ui/views/controls/menu/menu_runner_impl_cocoa.h"
#include "base/mac/mac_util.h"
#import "base/message_loop/message_pump_mac.h"
#import "skia/ext/skia_utils_mac.h"
#import "ui/base/cocoa/cocoa_base_utils.h"
#import "ui/base/cocoa/menu_controller.h"
#include "ui/base/l10n/l10n_util_mac.h"
#include "ui/base/models/menu_model.h"
#include "ui/events/base_event_utils.h"
#include "ui/events/event_utils.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/mac/coordinate_conversion.h"
#include "ui/gfx/platform_font_mac.h"
#include "ui/native_theme/native_theme.h"
#include "ui/strings/grit/ui_strings.h"
#include "ui/views/controls/menu/menu_config.h"
#include "ui/views/controls/menu/menu_runner_impl_adapter.h"
#include "ui/views/views_features.h"
#include "ui/views/widget/widget.h"
namespace {
NSImage* NewTagImage() {
static NSImage* new_tag = []() {
// 1. Make the attributed string.
NSString* badge_text = l10n_util::GetNSString(IDS_MENU_ITEM_NEW_BADGE);
// The preferred font is slightly smaller and slightly more bold than the
// menu font. The size change is required to make it look correct in the
// badge; we add a small degree of bold to prevent color smearing/blurring
// due to font smoothing. This ensures readability on all platforms and in
// both light and dark modes.
gfx::Font badge_font = gfx::Font(
new gfx::PlatformFontMac(gfx::PlatformFontMac::SystemFontType::kMenu));
badge_font =
badge_font.Derive(views::MenuConfig::kNewBadgeFontSizeAdjustment,
gfx::Font::NORMAL, gfx::Font::Weight::MEDIUM);
NSColor* badge_text_color = skia::SkColorToSRGBNSColor(
ui::NativeTheme::GetInstanceForNativeUi()->GetSystemColor(
ui::NativeTheme::kColorId_TextOnProminentButtonColor));
NSDictionary* badge_attrs = @{
NSFontAttributeName : badge_font.GetNativeFont(),
NSForegroundColorAttributeName : badge_text_color,
};
NSMutableAttributedString* badge_attr_string =
[[NSMutableAttributedString alloc] initWithString:badge_text
attributes:badge_attrs];
if (base::mac::IsOS10_10()) {
// The system font for 10.10 is Helvetica Neue, and when used for this
// "new tag" the letters look cramped. Track it out so that there's some
// breathing room. There is no tracking attribute, so instead add kerning
// to all but the last character.
[badge_attr_string
addAttribute:NSKernAttributeName
value:@0.4
range:NSMakeRange(0, [badge_attr_string length] - 1)];
}
// 2. Calculate the size required.
NSSize badge_size = [badge_attr_string size];
badge_size.width = trunc(badge_size.width);
badge_size.height = trunc(badge_size.height);
badge_size.width += 2 * views::MenuConfig::kNewBadgeInternalPadding +
2 * views::MenuConfig::kNewBadgeHorizontalMargin;
badge_size.height += views::MenuConfig::kNewBadgeInternalPaddingTopMac;
// 3. Craft the image.
return [[NSImage
imageWithSize:badge_size
flipped:NO
drawingHandler:^(NSRect dest_rect) {
NSRect badge_frame = NSInsetRect(
dest_rect, views::MenuConfig::kNewBadgeHorizontalMargin, 0);
NSBezierPath* rounded_badge_rect = [NSBezierPath
bezierPathWithRoundedRect:badge_frame
xRadius:views::MenuConfig::kNewBadgeCornerRadius
yRadius:views::MenuConfig::
kNewBadgeCornerRadius];
NSColor* badge_color = skia::SkColorToSRGBNSColor(
ui::NativeTheme::GetInstanceForNativeUi()->GetSystemColor(
ui::NativeTheme::kColorId_ProminentButtonColor));
[badge_color set];
[rounded_badge_rect fill];
NSPoint badge_text_location = NSMakePoint(
NSMinX(badge_frame) + views::MenuConfig::kNewBadgeInternalPadding,
NSMinY(badge_frame) +
views::MenuConfig::kNewBadgeInternalPaddingTopMac);
[badge_attr_string drawAtPoint:badge_text_location];
return YES;
}] retain];
}();
return new_tag;
}
} // namespace
@interface NewTagAttachmentCell : NSTextAttachmentCell
@end
@implementation NewTagAttachmentCell
- (instancetype)init {
if (self = [super init]) {
self.image = NewTagImage();
}
return self;
}
- (NSPoint)cellBaselineOffset {
return NSMakePoint(0, views::MenuConfig::kNewBadgeBaslineOffsetMac);
}
- (NSSize)cellSize {
return [self.image size];
}
@end
@interface MenuControllerDelegate : NSObject <MenuControllerCocoaDelegate>
@end
@implementation MenuControllerDelegate
- (void)controllerWillAddItem:(NSMenuItem*)menuItem
fromModel:(ui::MenuModel*)model
atIndex:(NSInteger)index {
static const bool feature_enabled =
base::FeatureList::IsEnabled(views::features::kEnableNewBadgeOnMenuItems);
if (!feature_enabled || !model->IsNewFeatureAt(index))
return;
// TODO(avi): When moving to 10.11 as the minimum macOS, switch to using
// NSTextAttachment's |image| and |bounds| properties and avoid the whole
// NSTextAttachmentCell subclassing mishegas.
base::scoped_nsobject<NSTextAttachment> attachment(
[[NSTextAttachment alloc] init]);
attachment.get().attachmentCell =
[[[NewTagAttachmentCell alloc] init] autorelease];
// Starting in 10.13, if an attributed string is set as a menu item title, and
// NSFontAttributeName is not specified for it, it is automatically rendered
// in a font matching other menu items. Prior to then, a menu item with no
// specified font is rendered in Helvetica. In addition, while the
// documentation says that -[NSFont menuFontOfSize:0] gives the standard menu
// font, that doesn't actually match up. Therefore, specify a font that
// visually matches.
NSDictionary* attrs = nil;
if (base::mac::IsAtMostOS10_12())
attrs = @{NSFontAttributeName : [NSFont menuFontOfSize:14]};
base::scoped_nsobject<NSMutableAttributedString> attrTitle(
[[NSMutableAttributedString alloc] initWithString:menuItem.title
attributes:attrs]);
[attrTitle
appendAttributedString:[NSAttributedString
attributedStringWithAttachment:attachment]];
menuItem.attributedTitle = attrTitle;
}
@end
namespace views {
namespace internal {
namespace {
......@@ -124,7 +285,7 @@ MenuRunnerImplInterface* MenuRunnerImplInterface::Create(
int32_t run_types,
base::RepeatingClosure on_menu_closed_callback) {
if ((run_types & MenuRunner::CONTEXT_MENU) &&
!(run_types & (MenuRunner::IS_NESTED | MenuRunner::FORCE_VIEWS))) {
!(run_types & (MenuRunner::IS_NESTED))) {
return new MenuRunnerImplCocoa(menu_model,
std::move(on_menu_closed_callback));
}
......@@ -139,8 +300,11 @@ MenuRunnerImplCocoa::MenuRunnerImplCocoa(
delete_after_run_(false),
closing_event_time_(base::TimeTicks()),
on_menu_closed_callback_(std::move(on_menu_closed_callback)) {
menu_controller_.reset([[MenuControllerCocoa alloc] initWithModel:menu
useWithPopUpButtonCell:NO]);
menu_delegate_.reset([[MenuControllerDelegate alloc] init]);
menu_controller_.reset([[MenuControllerCocoa alloc]
initWithModel:menu
delegate:menu_delegate_.get()
useWithPopUpButtonCell:NO]);
}
bool MenuRunnerImplCocoa::IsRunning() const {
......
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