| Index: chrome/browser/ui/views/tabs/tab_strip.cc
|
| diff --git a/chrome/browser/ui/views/tabs/tab_strip.cc b/chrome/browser/ui/views/tabs/tab_strip.cc
|
| index 564405dfbb3d268910180d3b9847176667414c3b..34f422f8b1236189025e3e56a860ac2d22e5997e 100644
|
| --- a/chrome/browser/ui/views/tabs/tab_strip.cc
|
| +++ b/chrome/browser/ui/views/tabs/tab_strip.cc
|
| @@ -19,6 +19,7 @@
|
| #include "chrome/browser/ui/layout_constants.h"
|
| #include "chrome/browser/ui/tabs/tab_strip_model.h"
|
| #include "chrome/browser/ui/view_ids.h"
|
| +#include "chrome/browser/ui/views/frame/browser_view.h"
|
| #include "chrome/browser/ui/views/tabs/stacked_tab_strip_layout.h"
|
| #include "chrome/browser/ui/views/tabs/tab.h"
|
| #include "chrome/browser/ui/views/tabs/tab_drag_controller.h"
|
| @@ -31,11 +32,16 @@
|
| #include "content/public/browser/user_metrics.h"
|
| #include "content/public/common/content_switches.h"
|
| #include "grit/theme_resources.h"
|
| +#include "third_party/skia/include/core/SkColorFilter.h"
|
| +#include "third_party/skia/include/effects/SkBlurMaskFilter.h"
|
| +#include "third_party/skia/include/effects/SkLayerDrawLooper.h"
|
| +#include "third_party/skia/include/pathops/SkPathOps.h"
|
| #include "ui/accessibility/ax_view_state.h"
|
| #include "ui/base/default_theme_provider.h"
|
| #include "ui/base/dragdrop/drag_drop_types.h"
|
| #include "ui/base/l10n/l10n_util.h"
|
| #include "ui/base/models/list_selection_model.h"
|
| +#include "ui/base/resource/material_design/material_design_controller.h"
|
| #include "ui/base/resource/resource_bundle.h"
|
| #include "ui/compositor/compositing_recorder.h"
|
| #include "ui/compositor/paint_recorder.h"
|
| @@ -126,6 +132,28 @@ int GetNewTabButtonWidth() {
|
| GetLayoutConstant(TABSTRIP_NEW_TAB_BUTTON_OVERLAP);
|
| }
|
|
|
| +skia::RefPtr<SkDrawLooper> CreateShadowDrawLooper(SkAlpha alpha) {
|
| + SkLayerDrawLooper::Builder looper_builder;
|
| + looper_builder.addLayer();
|
| +
|
| + SkLayerDrawLooper::LayerInfo layer_info;
|
| + layer_info.fPaintBits |= SkLayerDrawLooper::kMaskFilter_Bit;
|
| + layer_info.fPaintBits |= SkLayerDrawLooper::kColorFilter_Bit;
|
| + layer_info.fColorMode = SkXfermode::kDst_Mode;
|
| + layer_info.fOffset.set(0, 1);
|
| + skia::RefPtr<SkMaskFilter> blur_mask =
|
| + skia::AdoptRef(SkBlurMaskFilter::Create(
|
| + kNormal_SkBlurStyle, 0.5, SkBlurMaskFilter::kHighQuality_BlurFlag));
|
| + skia::RefPtr<SkColorFilter> color_filter =
|
| + skia::AdoptRef(SkColorFilter::CreateModeFilter(
|
| + SkColorSetA(SK_ColorBLACK, alpha), SkXfermode::kSrcIn_Mode));
|
| + SkPaint* layer_paint = looper_builder.addLayer(layer_info);
|
| + layer_paint->setMaskFilter(blur_mask.get());
|
| + layer_paint->setColorFilter(color_filter.get());
|
| +
|
| + return skia::AdoptRef(looper_builder.detachLooper());
|
| +}
|
| +
|
| // Animation delegate used for any automatic tab movement. Hides the tab if it
|
| // is not fully visible within the tabstrip area, to prevent overflow clipping.
|
| class TabAnimationDelegate : public gfx::AnimationDelegate {
|
| @@ -267,11 +295,22 @@ class NewTabButton : public views::ImageButton,
|
| // views::MaskedTargeterDelegate:
|
| bool GetHitTestMask(gfx::Path* mask) const override;
|
|
|
| + // Computes a path corresponding to the button's outer border for a given
|
| + // |scale| and stores it in |path|. |button_y| is used as the y-coordinate
|
| + // for the top of the button. If |extend_to_top| is true, the path is
|
| + // extended vertically to y = 0. The caller uses this for Fitts' Law purposes
|
| + // in maximized/fullscreen mode.
|
| + void GetBorderPath(float button_y,
|
| + float scale,
|
| + bool extend_to_top,
|
| + SkPath* path) const;
|
| +
|
| // Paints the fill region of the button into |canvas|, according to the
|
| - // supplied values from GetImage().
|
| + // supplied values from GetImage() and the given |fill| path.
|
| void PaintFill(bool pressed,
|
| double hover_value,
|
| float scale,
|
| + const SkPath& fill,
|
| gfx::Canvas* canvas) const;
|
|
|
| // Tab strip that contains this button.
|
| @@ -339,32 +378,77 @@ void NewTabButton::OnPaint(gfx::Canvas* canvas) {
|
| hover_value = hover_animation_->GetCurrentValue();
|
| const float scale = canvas->image_scale();
|
|
|
| - // Fill.
|
| - gfx::ImageSkia* mask =
|
| - GetThemeProvider()->GetImageSkiaNamed(IDR_NEWTAB_BUTTON_MASK);
|
| - // The canvas and mask have to use the same scale factor.
|
| - const float fill_canvas_scale = mask->HasRepresentation(scale) ?
|
| - scale : ui::GetScaleForScaleFactor(ui::SCALE_FACTOR_100P);
|
| - gfx::Canvas fill_canvas(GetNewTabButtonSize(), fill_canvas_scale, false);
|
| - PaintFill(pressed, hover_value, fill_canvas_scale, &fill_canvas);
|
| - gfx::ImageSkia image(fill_canvas.ExtractImageRep());
|
| - canvas->DrawImageInt(
|
| - gfx::ImageSkiaOperations::CreateMaskedImage(image, *mask), 0, 0);
|
| -
|
| - // Stroke. Draw the button border with a slight alpha.
|
| - static const SkAlpha kGlassFrameOverlayAlpha = 178;
|
| - static const SkAlpha kOpaqueFrameOverlayAlpha = 230;
|
| - const SkAlpha alpha = GetWidget()->ShouldWindowContentsBeTransparent() ?
|
| - kGlassFrameOverlayAlpha : kOpaqueFrameOverlayAlpha;
|
| - const int overlay_id = pressed ? IDR_NEWTAB_BUTTON_P : IDR_NEWTAB_BUTTON;
|
| - canvas->DrawImageInt(*GetThemeProvider()->GetImageSkiaNamed(overlay_id), 0, 0,
|
| - alpha);
|
| + SkPath fill;
|
| + if (ui::MaterialDesignController::IsModeMaterial()) {
|
| + // Fill.
|
| + fill.moveTo(9.75 * scale, 16 * scale);
|
| + fill.rCubicTo(-0.75 * scale, 0, -1.625 * scale, -0.5 * scale, -2 * scale,
|
| + -1.5 * scale);
|
| + fill.rLineTo(-5.75 * scale, -12.5 * scale);
|
| + fill.rCubicTo(0, -0.5 * scale, 0.25 * scale, -scale, scale, -scale);
|
| + fill.rLineTo(23.25 * scale, 0);
|
| + fill.rCubicTo(0.75 * scale, 0, 1.625 * scale, 0.5 * scale, 2 * scale,
|
| + 1.5 * scale);
|
| + fill.rLineTo(5.75 * scale, 12.5 * scale);
|
| + fill.rCubicTo(0, 0.5 * scale, -0.25 * scale, scale, -scale, scale);
|
| + fill.close();
|
| + PaintFill(pressed, hover_value, scale, fill, canvas);
|
| +
|
| + // Stroke.
|
| + gfx::ScopedCanvas scoped_canvas(canvas);
|
| + canvas->UndoDeviceScaleFactor();
|
| + SkPath stroke;
|
| + GetBorderPath(0, scale, false, &stroke);
|
| + // We want to draw a drop shadow either inside or outside the stroke,
|
| + // depending on whether we're pressed; so, either clip out what's outside
|
| + // the stroke, or clip out the fill inside it.
|
| + if (pressed)
|
| + canvas->ClipPath(stroke, true);
|
| + Op(stroke, fill, kDifference_SkPathOp, &stroke);
|
| + if (!pressed)
|
| + canvas->sk_canvas()->clipPath(fill, SkRegion::kDifference_Op, true);
|
| + // Now draw the stroke and shadow; the stroke will always be visible, while
|
| + // the shadow will be affected by the clip we set above.
|
| + SkPaint paint;
|
| + paint.setAntiAlias(true);
|
| + skia::RefPtr<SkDrawLooper> stroke_looper = CreateShadowDrawLooper(0x8C);
|
| + paint.setLooper(stroke_looper.get());
|
| + paint.setColor(SkColorSetA(SK_ColorBLACK, pressed ? 0x38 : 0x27));
|
| + canvas->DrawPath(stroke, paint);
|
| + } else {
|
| + // Fill.
|
| + gfx::ImageSkia* mask =
|
| + GetThemeProvider()->GetImageSkiaNamed(IDR_NEWTAB_BUTTON_MASK);
|
| + // The canvas and mask have to use the same scale factor.
|
| + const float fill_canvas_scale = mask->HasRepresentation(scale) ?
|
| + scale : ui::GetScaleForScaleFactor(ui::SCALE_FACTOR_100P);
|
| + gfx::Canvas fill_canvas(GetNewTabButtonSize(), fill_canvas_scale, false);
|
| + PaintFill(pressed, hover_value, fill_canvas_scale, fill, &fill_canvas);
|
| + gfx::ImageSkia image(fill_canvas.ExtractImageRep());
|
| + canvas->DrawImageInt(
|
| + gfx::ImageSkiaOperations::CreateMaskedImage(image, *mask), 0, 0);
|
| +
|
| + // Stroke. Draw the button border with a slight alpha.
|
| + static const SkAlpha kGlassFrameOverlayAlpha = 178;
|
| + static const SkAlpha kOpaqueFrameOverlayAlpha = 230;
|
| + const SkAlpha alpha = GetWidget()->ShouldWindowContentsBeTransparent() ?
|
| + kGlassFrameOverlayAlpha : kOpaqueFrameOverlayAlpha;
|
| + const int overlay_id = pressed ? IDR_NEWTAB_BUTTON_P : IDR_NEWTAB_BUTTON;
|
| + canvas->DrawImageInt(*GetThemeProvider()->GetImageSkiaNamed(overlay_id), 0,
|
| + 0, alpha);
|
| + }
|
| }
|
|
|
| bool NewTabButton::GetHitTestMask(gfx::Path* mask) const {
|
| DCHECK(mask);
|
|
|
| - if (tab_strip_->SizeTabButtonToTopOfTabStrip()) {
|
| + if (ui::MaterialDesignController::IsModeMaterial()) {
|
| + SkPath border;
|
| + const float scale = GetWidget()->GetCompositor()->device_scale_factor();
|
| + GetBorderPath(TabStrip::kNewTabButtonVerticalOffset * scale, scale,
|
| + tab_strip_->SizeTabButtonToTopOfTabStrip(), &border);
|
| + mask->addPath(border, SkMatrix::MakeScale(1 / scale));
|
| + } else if (tab_strip_->SizeTabButtonToTopOfTabStrip()) {
|
| // When the button is sized to the top of the tab strip, we want the hit
|
| // test mask to be defined as the complete (rectangular) bounds of the
|
| // button.
|
| @@ -393,56 +477,134 @@ bool NewTabButton::GetHitTestMask(gfx::Path* mask) const {
|
| return true;
|
| }
|
|
|
| +void NewTabButton::GetBorderPath(float button_y,
|
| + float scale,
|
| + bool extend_to_top,
|
| + SkPath* path) const {
|
| + path->moveTo(9.75 * scale - 1, button_y + 16 * scale + 1);
|
| + path->rCubicTo(-0.75 * scale, 0, -1.625 * scale, -0.5 * scale, -2 * scale,
|
| + -1.5 * scale);
|
| + path->rLineTo(-5.75 * scale, -12.5 * scale);
|
| + if (extend_to_top) {
|
| + // Create the vertical extension by extending the side diagonals at the
|
| + // upper left and lower right corners until they reach the top and bottom of
|
| + // the border, respectively (in other words, "un-round-off" those corners
|
| + // and turn them into sharp points). Then extend upward from the corner
|
| + // points to the top of the bounds.
|
| + const float dy = scale + 2;
|
| + const float dx = 11.5 / 25 * dy;
|
| + path->rLineTo(-dx, -dy);
|
| + path->rLineTo(0, -button_y - scale + 1);
|
| + path->lineTo(34 * scale + 1 + dx, 0);
|
| + path->rLineTo(0, button_y + 16 * scale + 1);
|
| + } else {
|
| + path->rCubicTo(-0.5 * scale, -1.125 * scale, 0.5 * scale, -scale - 2, scale,
|
| + -scale - 2);
|
| + path->rLineTo(23.25 * scale + 2, 0);
|
| + path->rCubicTo(0.75 * scale, 0, 1.625 * scale, 0.5 * scale, 2 * scale,
|
| + 1.5 * scale);
|
| + path->rLineTo(5.75 * scale, 12.5 * scale);
|
| + path->rCubicTo(0.5 * scale, 1.125 * scale, -0.5 * scale, scale + 2, -scale,
|
| + scale + 2);
|
| + }
|
| + path->close();
|
| +}
|
| +
|
| void NewTabButton::PaintFill(bool pressed,
|
| double hover_value,
|
| float scale,
|
| + const SkPath& fill,
|
| gfx::Canvas* canvas) const {
|
| bool custom_image;
|
| const int bg_id = tab_strip_->GetBackgroundResourceId(&custom_image);
|
|
|
| + gfx::ScopedCanvas scoped_canvas(canvas);
|
| +
|
| + const bool md = ui::MaterialDesignController::IsModeMaterial();
|
| + if (md) {
|
| + canvas->UndoDeviceScaleFactor();
|
| +
|
| + // For unpressed buttons, draw the fill and its shadow.
|
| + if (!pressed) {
|
| + gfx::ScopedCanvas scoped_canvas(canvas);
|
| +
|
| + // For custom themes, clip out the fill path itself so only the shadow
|
| + // around it is drawn. We'll draw the fill image below.
|
| + if (custom_image)
|
| + canvas->sk_canvas()->clipPath(fill, SkRegion::kDifference_Op, true);
|
| +
|
| + SkPaint paint;
|
| + paint.setAntiAlias(true);
|
| + skia::RefPtr<SkDrawLooper> looper = CreateShadowDrawLooper(0x26);
|
| + paint.setLooper(looper.get());
|
| + paint.setColor(Tab::kInactiveTabColor);
|
| + canvas->DrawPath(fill, paint);
|
| + }
|
| +
|
| + // Clip to just the fill region for any theme drawing and hover/pressed
|
| + // state overlay drawing below. In non-MD mode, this is done on the caller
|
| + // side using image masking operations.
|
| + canvas->ClipPath(fill, true);
|
| + canvas->sk_canvas()->scale(scale, scale);
|
| + }
|
| +
|
| // Draw the fill background image.
|
| const gfx::Size size(GetNewTabButtonSize());
|
| - const ui::ThemeProvider* theme_provider = GetThemeProvider();
|
| - gfx::ImageSkia* background = theme_provider->GetImageSkiaNamed(bg_id);
|
| - // For custom tab backgrounds the background starts at the top of the tab
|
| - // strip. Otherwise the background starts at the top of the frame.
|
| - const int offset_y = theme_provider->HasCustomImage(bg_id) ?
|
| - -GetLayoutConstant(TAB_TOP_EXCLUSION_HEIGHT) : background_offset_.y();
|
| -
|
| - // The new tab background is mirrored in RTL mode, but the theme background
|
| - // should never be mirrored. Mirror it here to compensate.
|
| - float x_scale = 1.0f;
|
| - int x = GetMirroredX() + background_offset_.x();
|
| - if (base::i18n::IsRTL()) {
|
| - x_scale = -1.0f;
|
| - // Offset by |width| such that the same region is painted as if there was
|
| - // no flip.
|
| - x += size.width();
|
| - }
|
| - canvas->TileImageInt(*background, x,
|
| - TabStrip::kNewTabButtonVerticalOffset + offset_y,
|
| - x_scale, 1.0f, 0, 0, size.width(), size.height());
|
| -
|
| - // Adjust the alpha of the fill to match that of inactive tabs (except for
|
| - // pressed buttons, which get a different value).
|
| - static const SkAlpha kPressedAlpha = 145;
|
| - const SkAlpha alpha =
|
| - pressed ? kPressedAlpha : tab_strip_->GetInactiveAlpha(true);
|
| - if (alpha != 255) {
|
| - SkPaint paint;
|
| - paint.setAlpha(alpha);
|
| - paint.setXfermodeMode(SkXfermode::kDstIn_Mode);
|
| - paint.setStyle(SkPaint::kFill_Style);
|
| - canvas->DrawRect(gfx::Rect(size), paint);
|
| + if (custom_image || !md) {
|
| + const ui::ThemeProvider* theme_provider = GetThemeProvider();
|
| + gfx::ImageSkia* background = theme_provider->GetImageSkiaNamed(bg_id);
|
| + // For custom tab backgrounds the background starts at the top of the tab
|
| + // strip. Otherwise the background starts at the top of the frame.
|
| + const int offset_y = theme_provider->HasCustomImage(bg_id) ?
|
| + -GetLayoutConstant(TAB_TOP_EXCLUSION_HEIGHT) : background_offset_.y();
|
| +
|
| + // The new tab background is mirrored in RTL mode, but the theme background
|
| + // should never be mirrored. Mirror it here to compensate.
|
| + float x_scale = 1.0f;
|
| + int x = GetMirroredX() + background_offset_.x();
|
| + if (base::i18n::IsRTL()) {
|
| + x_scale = -1.0f;
|
| + // Offset by |width| such that the same region is painted as if there was
|
| + // no flip.
|
| + x += size.width();
|
| + }
|
| + canvas->TileImageInt(*background, x,
|
| + TabStrip::kNewTabButtonVerticalOffset + offset_y,
|
| + x_scale, 1.0f, 0, 0, size.width(), size.height());
|
| +
|
| + // For non-MD, adjust the alpha of the fill to match that of inactive tabs
|
| + // (except for pressed buttons, which get a different value). For MD, we do
|
| + // this with an opacity recorder in TabStrip::PaintChildren() so the fill
|
| + // and stroke are both affected, to better match how tabs are handled, but
|
| + // in non-MD, the button stroke is already lighter than the tab stroke, and
|
| + // using the opacity recorder washes it out too much.
|
| + static const SkAlpha kPressedAlpha = 145;
|
| + const SkAlpha alpha =
|
| + pressed ? kPressedAlpha : tab_strip_->GetInactiveAlpha(true);
|
| + if (alpha != 255 && !md) {
|
| + SkPaint paint;
|
| + paint.setAlpha(alpha);
|
| + paint.setXfermodeMode(SkXfermode::kDstIn_Mode);
|
| + paint.setStyle(SkPaint::kFill_Style);
|
| + canvas->DrawRect(gfx::Rect(size), paint);
|
| + }
|
| }
|
|
|
| // White highlight on hover.
|
| if (hover_value) {
|
| const int alpha =
|
| - gfx::Tween::LinearIntValueBetween(hover_value, 0x00, 0x40);
|
| + gfx::Tween::LinearIntValueBetween(hover_value, 0x00, md ? 0x4D : 0x40);
|
| canvas->FillRect(GetLocalBounds(),
|
| SkColorSetA(SK_ColorWHITE, static_cast<SkAlpha>(alpha)));
|
| }
|
| +
|
| + // For MD, most states' opacities are adjusted using an opacity recorder in
|
| + // TabStrip::PaintChildren(), but the pressed state is excluded there and
|
| + // instead rendered using a dark overlay here. This produces a different
|
| + // effect than for non-MD, and avoiding the use of the opacity recorder keeps
|
| + // the stroke more visible in this state.
|
| + if (md && pressed)
|
| + canvas->FillRect(GetLocalBounds(), SkColorSetA(SK_ColorBLACK, 0x14));
|
| }
|
|
|
| ///////////////////////////////////////////////////////////////////////////////
|
| @@ -1270,13 +1432,11 @@ void TabStrip::PaintChildren(const ui::PaintContext& context) {
|
| Tabs selected_tabs;
|
|
|
| {
|
| - const chrome::HostDesktopType host_desktop_type =
|
| - chrome::GetHostDesktopTypeForNativeView(GetWidget()->GetNativeView());
|
| - const uint8_t inactive_tab_alpha =
|
| - (host_desktop_type == chrome::HOST_DESKTOP_TYPE_ASH) ?
|
| - GetInactiveAlpha(false) : 255;
|
| + // TODO(pkasting): This results in greyscale AA for the tab titles. Work
|
| + // with Mike Reed to fix, or else convert to passing the tabs a memory
|
| + // canvas to paint on and then manually compositing.
|
| ui::CompositingRecorder opacity_recorder(context, size(),
|
| - inactive_tab_alpha);
|
| + GetInactiveAlpha(false));
|
|
|
| PaintClosingTabs(tab_count(), context);
|
|
|
| @@ -1319,28 +1479,6 @@ void TabStrip::PaintChildren(const ui::PaintContext& context) {
|
| }
|
| }
|
|
|
| - if (GetWidget()->ShouldWindowContentsBeTransparent()) {
|
| - ui::PaintRecorder recorder(context, size());
|
| - // Make sure non-active tabs are somewhat transparent.
|
| - SkPaint paint;
|
| - // If there are multiple tabs selected, fade non-selected tabs more to make
|
| - // the selected tabs more noticable.
|
| - uint8_t alpha = GetInactiveAlpha(false);
|
| - paint.setColor(SkColorSetA(SK_ColorWHITE, alpha));
|
| - paint.setXfermodeMode(SkXfermode::kDstIn_Mode);
|
| - paint.setStyle(SkPaint::kFill_Style);
|
| -
|
| - gfx::Rect bounds(GetLocalBounds());
|
| - // The tab graphics include some shadows at the top, plus a 1 pixel top
|
| - // stroke. Exclude this region when trying to make tabs transparent as it's
|
| - // transparent enough already, and drawing in this region can overlap the
|
| - // avatar button, leading to visual artifacts. Also exclude the toolbar
|
| - // overlap region at the bottom.
|
| - gfx::Insets tab_insets(GetLayoutInsets(TAB));
|
| - bounds.Inset(0, tab_insets.top(), 0, tab_insets.bottom());
|
| - recorder.canvas()->DrawRect(bounds, paint);
|
| - }
|
| -
|
| // Now selected but not active. We don't want these dimmed if using native
|
| // frame, so they're painted after initial pass.
|
| for (size_t i = 0; i < selected_tabs.size(); ++i)
|
| @@ -1351,7 +1489,16 @@ void TabStrip::PaintChildren(const ui::PaintContext& context) {
|
| active_tab->Paint(context);
|
|
|
| // Paint the New Tab button.
|
| - newtab_button_->Paint(context);
|
| + const bool md = ui::MaterialDesignController::IsModeMaterial();
|
| + if (md && (newtab_button_->state() != views::CustomButton::STATE_PRESSED)) {
|
| + // Match the inactive tab opacity for non-pressed states. See comments in
|
| + // NewTabButton::PaintFill() for why we don't do this for the pressed state.
|
| + ui::CompositingRecorder opacity_recorder(context, size(),
|
| + GetInactiveAlpha(true));
|
| + newtab_button_->Paint(context);
|
| + } else {
|
| + newtab_button_->Paint(context);
|
| + }
|
|
|
| // And the dragged tabs.
|
| for (size_t i = 0; i < tabs_dragging.size(); ++i)
|
| @@ -1360,6 +1507,18 @@ void TabStrip::PaintChildren(const ui::PaintContext& context) {
|
| // If the active tab is being dragged, it goes last.
|
| if (active_tab && is_dragging)
|
| active_tab->Paint(context);
|
| +
|
| + if (md) {
|
| + ui::PaintRecorder recorder(context, size());
|
| + gfx::Canvas* canvas = recorder.canvas();
|
| + if (active_tab) {
|
| + canvas->sk_canvas()->clipRect(
|
| + gfx::RectToSkRect(active_tab->GetMirroredBounds()),
|
| + SkRegion::kDifference_Op);
|
| + }
|
| + BrowserView::Paint1pxHorizontalLine(
|
| + canvas, SkColorSetA(SK_ColorBLACK, 0x40), GetLocalBounds(), true);
|
| + }
|
| }
|
|
|
| const char* TabStrip::GetClassName() const {
|
|
|