| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import "chrome/browser/ui/cocoa/tabs/tab_view.h" |
| |
| #include "base/i18n/rtl.h" |
| #include "base/logging.h" |
| #include "base/mac/sdk_forward_declarations.h" |
| #include "base/strings/sys_string_conversions.h" |
| #include "chrome/browser/themes/theme_properties.h" |
| #include "chrome/browser/themes/theme_service.h" |
| #import "chrome/browser/ui/cocoa/tabs/alert_indicator_button_cocoa.h" |
| #import "chrome/browser/ui/cocoa/tabs/tab_controller.h" |
| #import "chrome/browser/ui/cocoa/tabs/tab_window_controller.h" |
| #import "chrome/browser/ui/cocoa/themed_window.h" |
| #import "chrome/browser/ui/cocoa/view_id_util.h" |
| #include "chrome/grit/generated_resources.h" |
| #include "chrome/grit/theme_resources.h" |
| #include "skia/ext/skia_utils_mac.h" |
| #import "third_party/google_toolbox_for_mac/src/AppKit/GTMFadeTruncatingTextFieldCell.h" |
| #import "ui/base/cocoa/nsgraphics_context_additions.h" |
| #import "ui/base/cocoa/nsview_additions.h" |
| #include "ui/base/cocoa/three_part_image.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/base/material_design/material_design_controller.h" |
| #include "ui/base/resource/resource_bundle.h" |
| #include "ui/gfx/scoped_ns_graphics_context_save_gstate_mac.h" |
| |
| namespace { |
| |
| // The color of the icons in dark mode theme. |
| const SkColor kDarkModeIconColor = SkColorSetARGB(0xFF, 0xC4, 0xC4, 0xC4); |
| |
| } // namespace |
| |
| // The amount of time in seconds during which each type of glow increases, holds |
| // steady, and decreases, respectively. |
| const NSTimeInterval kHoverShowDuration = 0.2; |
| const NSTimeInterval kHoverHoldDuration = 0.02; |
| const NSTimeInterval kHoverHideDuration = 0.4; |
| const NSTimeInterval kAlertShowDuration = 0.4; |
| const NSTimeInterval kAlertHoldDuration = 0.4; |
| const NSTimeInterval kAlertHideDuration = 0.4; |
| |
| // The default time interval in seconds between glow updates (when |
| // increasing/decreasing). |
| const NSTimeInterval kGlowUpdateInterval = 0.025; |
| |
| // The intensity of the white overlay when hovering the mouse over a tab. |
| const CGFloat kMouseHoverWhiteValue = 1.0; |
| const CGFloat kMouseHoverWhiteValueIncongito = 0.3; |
| |
| // This is used to judge whether the mouse has moved during rapid closure; if it |
| // has moved less than the threshold, we want to close the tab. |
| const CGFloat kRapidCloseDist = 2.5; |
| |
| @interface NSView (PrivateAPI) |
| // Called by AppKit to check if dragging this view should move the window. |
| // NSButton overrides this method in the same way so dragging window buttons |
| // has no effect. NSView implementation returns NSZeroRect so the whole view |
| // area can be dragged. |
| - (NSRect)_opaqueRectForWindowMoveWhenInTitlebar; |
| @end |
| |
| // This class contains the logic for drawing Material Design tab images. The |
| // |setTabEdgeStrokeColor| method is overridden by |TabHeavyImageMaker| to draw |
| // high-contrast tabs. |
| @interface TabImageMaker : NSObject |
| + (void)drawTabLeftMaskImage; |
| + (void)drawTabRightMaskImage; |
| + (void)drawTabLeftEdgeImage; |
| + (void)drawTabMiddleEdgeImage; |
| + (void)drawTabRightEdgeImage; |
| + (void)setTabEdgeStrokeColor; |
| @end |
| |
| @interface TabHeavyImageMaker : TabImageMaker |
| + (void)setTabEdgeStrokeColor; |
| @end |
| |
| @interface TabController(Private) |
| // The TabView's close button. |
| - (HoverCloseButton*)closeButton; |
| @end |
| |
| extern NSString* const _Nonnull NSWorkspaceAccessibilityDisplayOptionsDidChangeNotification; |
| |
| namespace { |
| |
| enum StrokeType { |
| STROKE_NORMAL, |
| STROKE_HEAVY, |
| }; |
| |
| NSImage* imageForResourceID(int resource_id, StrokeType stroke_type) { |
| CGFloat imageWidth = resource_id == IDR_TAB_ACTIVE_CENTER ? 1 : 18; |
| SEL theSelector = 0; |
| switch (resource_id) { |
| case IDR_TAB_ACTIVE_LEFT: |
| theSelector = @selector(drawTabLeftEdgeImage); |
| break; |
| |
| case IDR_TAB_ACTIVE_CENTER: |
| theSelector = @selector(drawTabMiddleEdgeImage); |
| break; |
| |
| case IDR_TAB_ACTIVE_RIGHT: |
| theSelector = @selector(drawTabRightEdgeImage); |
| break; |
| |
| case IDR_TAB_ALPHA_LEFT: |
| theSelector = @selector(drawTabLeftMaskImage); |
| break; |
| |
| case IDR_TAB_ALPHA_RIGHT: |
| theSelector = @selector(drawTabRightMaskImage); |
| break; |
| } |
| DCHECK(theSelector); |
| |
| Class makerClass = stroke_type == STROKE_HEAVY ? [TabHeavyImageMaker class] |
| : [TabImageMaker class]; |
| base::scoped_nsobject<NSCustomImageRep> imageRep([[NSCustomImageRep alloc] |
| initWithDrawSelector:theSelector |
| delegate:makerClass]); |
| |
| NSImage* newTabButtonImage = |
| [[[NSImage alloc] initWithSize:NSMakeSize(imageWidth, 29)] autorelease]; |
| [newTabButtonImage setCacheMode:NSImageCacheAlways]; |
| [newTabButtonImage addRepresentation:imageRep]; |
| |
| return newTabButtonImage; |
| } |
| |
| ui::ThreePartImage& GetMaskImage() { |
| CR_DEFINE_STATIC_LOCAL( |
| ui::ThreePartImage, mask, |
| (imageForResourceID(IDR_TAB_ALPHA_LEFT, STROKE_NORMAL), nullptr, |
| imageForResourceID(IDR_TAB_ALPHA_RIGHT, STROKE_NORMAL))); |
| |
| return mask; |
| } |
| |
| ui::ThreePartImage& GetStrokeImage(bool active, StrokeType stroke_type) { |
| CR_DEFINE_STATIC_LOCAL( |
| ui::ThreePartImage, stroke, |
| (imageForResourceID(IDR_TAB_ACTIVE_LEFT, STROKE_NORMAL), |
| imageForResourceID(IDR_TAB_ACTIVE_CENTER, STROKE_NORMAL), |
| imageForResourceID(IDR_TAB_ACTIVE_RIGHT, STROKE_NORMAL))); |
| CR_DEFINE_STATIC_LOCAL( |
| ui::ThreePartImage, heavyStroke, |
| (imageForResourceID(IDR_TAB_ACTIVE_LEFT, STROKE_HEAVY), |
| imageForResourceID(IDR_TAB_ACTIVE_CENTER, STROKE_HEAVY), |
| imageForResourceID(IDR_TAB_ACTIVE_RIGHT, STROKE_HEAVY))); |
| |
| return stroke_type == STROKE_HEAVY ? heavyStroke : stroke; |
| } |
| |
| CGFloat LineWidthFromContext(CGContextRef context) { |
| CGRect unitRect = CGRectMake(0.0, 0.0, 1.0, 1.0); |
| CGRect deviceRect = CGContextConvertRectToDeviceSpace(context, unitRect); |
| return 1.0 / deviceRect.size.height; |
| } |
| |
| } // namespace |
| |
| @interface TabView(Private) |
| |
| - (void)resetLastGlowUpdateTime; |
| - (NSTimeInterval)timeElapsedSinceLastGlowUpdate; |
| - (void)adjustGlowValue; |
| |
| @end // TabView(Private) |
| |
| @implementation TabView |
| |
| @synthesize state = state_; |
| @synthesize hoverAlpha = hoverAlpha_; |
| @synthesize alertAlpha = alertAlpha_; |
| @synthesize closing = closing_; |
| |
| + (CGFloat)maskImageFillHeight { |
| // Return the height of the "mask on" part of the mask bitmap. |
| return [TabController defaultTabHeight] - 1; |
| } |
| |
| - (id)initWithFrame:(NSRect)frame |
| controller:(TabController*)controller |
| closeButton:(HoverCloseButton*)closeButton { |
| self = [super initWithFrame:frame]; |
| if (self) { |
| controller_ = controller; |
| closeButton_ = closeButton; |
| |
| // Make a text field for the title, but don't add it as a subview. |
| // We will use the cell to draw the text directly into our layer, |
| // so that we can get font smoothing enabled. |
| titleView_.reset([[NSTextField alloc] init]); |
| [titleView_ setAutoresizingMask:NSViewWidthSizable]; |
| base::scoped_nsobject<GTMFadeTruncatingTextFieldCell> labelCell( |
| [[GTMFadeTruncatingTextFieldCell alloc] initTextCell:@"Label"]); |
| [labelCell setControlSize:NSSmallControlSize]; |
| [titleView_ setCell:labelCell]; |
| titleViewCell_ = labelCell; |
| |
| [self setWantsLayer:YES]; // -drawFill: needs a layer. |
| |
| if (&NSWorkspaceAccessibilityDisplayOptionsDidChangeNotification) { |
| NSNotificationCenter* center = |
| [[NSWorkspace sharedWorkspace] notificationCenter]; |
| [center |
| addObserver:self |
| selector:@selector(accessibilityOptionsDidChange:) |
| name: |
| NSWorkspaceAccessibilityDisplayOptionsDidChangeNotification |
| object:nil]; |
| } |
| } |
| return self; |
| } |
| |
| - (void)dealloc { |
| // Cancel any delayed requests that may still be pending (drags or hover). |
| [NSObject cancelPreviousPerformRequestsWithTarget:self]; |
| if (&NSWorkspaceAccessibilityDisplayOptionsDidChangeNotification) { |
| NSNotificationCenter* center = |
| [[NSWorkspace sharedWorkspace] notificationCenter]; |
| [center removeObserver:self]; |
| } |
| [super dealloc]; |
| } |
| |
| // Called by AppKit to check if dragging this view should move the window. |
| // NSButton overrides this method in the same way so dragging window buttons |
| // has no effect. |
| - (NSRect)_opaqueRectForWindowMoveWhenInTitlebar { |
| return [self bounds]; |
| } |
| |
| // Called to obtain the context menu for when the user hits the right mouse |
| // button (or control-clicks). (Note that -rightMouseDown: is *not* called for |
| // control-click.) |
| - (NSMenu*)menu { |
| if ([self isClosing]) |
| return nil; |
| |
| return [controller_ menu]; |
| } |
| |
| - (void)resizeSubviewsWithOldSize:(NSSize)oldBoundsSize { |
| [super resizeSubviewsWithOldSize:oldBoundsSize]; |
| // Called when our view is resized. If it gets too small, start by hiding |
| // the close button and only show it if tab is selected. Eventually, hide the |
| // icon as well. |
| [controller_ updateVisibility]; |
| } |
| |
| // Overridden so that mouse clicks come to this view (the parent of the |
| // hierarchy) first. We want to handle clicks and drags in this class and |
| // leave the background button for display purposes only. |
| - (BOOL)acceptsFirstMouse:(NSEvent*)theEvent { |
| return YES; |
| } |
| |
| - (void)mouseEntered:(NSEvent*)theEvent { |
| isMouseInside_ = YES; |
| [self resetLastGlowUpdateTime]; |
| [self adjustGlowValue]; |
| } |
| |
| - (void)mouseMoved:(NSEvent*)theEvent { |
| if (state_ == NSOffState) { |
| hoverPoint_ = [self convertPoint:[theEvent locationInWindow] fromView:nil]; |
| [self setNeedsDisplay:YES]; |
| } |
| } |
| |
| - (void)mouseExited:(NSEvent*)theEvent { |
| isMouseInside_ = NO; |
| hoverHoldEndTime_ = |
| [NSDate timeIntervalSinceReferenceDate] + kHoverHoldDuration; |
| [self resetLastGlowUpdateTime]; |
| [self adjustGlowValue]; |
| } |
| |
| - (void)setTrackingEnabled:(BOOL)enabled { |
| if (![closeButton_ isHidden]) { |
| [closeButton_ setTrackingEnabled:enabled]; |
| } |
| } |
| |
| // Determines which view a click in our frame actually hit. It's either this |
| // view or one of the child buttons. |
| - (NSView*)hitTest:(NSPoint)aPoint { |
| NSView* const defaultHitTestResult = [super hitTest:aPoint]; |
| if ([defaultHitTestResult isKindOfClass:[NSButton class]]) |
| return defaultHitTestResult; |
| |
| NSPoint viewPoint = [self convertPoint:aPoint fromView:[self superview]]; |
| NSRect maskRect = [self bounds]; |
| maskRect.size.height = [TabView maskImageFillHeight]; |
| return GetMaskImage().HitTest(viewPoint, maskRect) ? self : nil; |
| } |
| |
| // Handle clicks and drags in this button. We get here because we have |
| // overridden acceptsFirstMouse: and the click is within our bounds. |
| - (void)mouseDown:(NSEvent*)theEvent { |
| if ([self isClosing]) |
| return; |
| |
| // Record the point at which this event happened. This is used by other mouse |
| // events that are dispatched from |-maybeStartDrag::|. |
| mouseDownPoint_ = [theEvent locationInWindow]; |
| |
| // Record the state of the close button here, because selecting the tab will |
| // unhide it. |
| BOOL closeButtonActive = ![closeButton_ isHidden]; |
| |
| // During the tab closure animation (in particular, during rapid tab closure), |
| // we may get incorrectly hit with a mouse down. If it should have gone to the |
| // close button, we send it there -- it should then track the mouse, so we |
| // don't have to worry about mouse ups. |
| if (closeButtonActive && [controller_ inRapidClosureMode]) { |
| NSPoint hitLocation = [[self superview] convertPoint:mouseDownPoint_ |
| fromView:nil]; |
| if ([self hitTest:hitLocation] == closeButton_) { |
| [closeButton_ mouseDown:theEvent]; |
| return; |
| } |
| } |
| |
| // If the tab gets torn off, the tab controller will be removed from the tab |
| // strip and then deallocated. This will also result in *us* being |
| // deallocated. Both these are bad, so we prevent this by retaining the |
| // controller. |
| base::scoped_nsobject<TabController> controller([controller_ retain]); |
| |
| // Try to initiate a drag. This will spin a custom event loop and may |
| // dispatch other mouse events. |
| [controller_ maybeStartDrag:theEvent forTab:controller]; |
| |
| // The custom loop has ended, so clear the point. |
| mouseDownPoint_ = NSZeroPoint; |
| } |
| |
| - (void)mouseUp:(NSEvent*)theEvent { |
| // Check for rapid tab closure. |
| if ([theEvent type] == NSLeftMouseUp) { |
| NSPoint upLocation = [theEvent locationInWindow]; |
| CGFloat dx = upLocation.x - mouseDownPoint_.x; |
| CGFloat dy = upLocation.y - mouseDownPoint_.y; |
| |
| // During rapid tab closure (mashing tab close buttons), we may get hit |
| // with a mouse down. As long as the mouse up is over the close button, |
| // and the mouse hasn't moved too much, we close the tab. |
| if (![closeButton_ isHidden] && |
| (dx*dx + dy*dy) <= kRapidCloseDist*kRapidCloseDist && |
| [controller_ inRapidClosureMode]) { |
| NSPoint hitLocation = |
| [[self superview] convertPoint:[theEvent locationInWindow] |
| fromView:nil]; |
| if ([self hitTest:hitLocation] == closeButton_) { |
| [controller_ closeTab:self]; |
| return; |
| } |
| } |
| } |
| |
| // Except in the rapid tab closure case, mouseDown: triggers a nested run loop |
| // that swallows the mouseUp: event. There's a bug in AppKit that sends |
| // mouseUp: callbacks to inappropriate views, so it's doubly important that |
| // this method doesn't do anything. https://crbug.com/511095. |
| [super mouseUp:theEvent]; |
| } |
| |
| - (void)otherMouseUp:(NSEvent*)theEvent { |
| if ([self isClosing]) |
| return; |
| |
| // Support middle-click-to-close. |
| if ([theEvent buttonNumber] == 2) { |
| // |-hitTest:| takes a location in the superview's coordinates. |
| NSPoint upLocation = |
| [[self superview] convertPoint:[theEvent locationInWindow] |
| fromView:nil]; |
| // If the mouse up occurred in our view or over the close button, then |
| // close. |
| if ([self hitTest:upLocation]) |
| [controller_ closeTab:self]; |
| } |
| } |
| |
| // Returns the color used to draw the background of a tab. |selected| selects |
| // between the foreground and background tabs. |
| - (NSColor*)backgroundColorForSelected:(bool)selected { |
| const ui::ThemeProvider* themeProvider = [[self window] themeProvider]; |
| if (!themeProvider) |
| return [[self window] backgroundColor]; |
| |
| int bitmapResources[2][2] = { |
| // Background window. |
| { |
| IDR_THEME_TAB_BACKGROUND_INACTIVE, // Background tab. |
| IDR_THEME_TOOLBAR_INACTIVE, // Active tab. |
| }, |
| // Currently focused window. |
| { |
| IDR_THEME_TAB_BACKGROUND, // Background tab. |
| IDR_THEME_TOOLBAR, // Active tab. |
| }, |
| }; |
| |
| // Themes don't have an inactive image so only look for one if there's no |
| // theme. |
| bool active = |
| [[self window] isMainWindow] || !themeProvider->UsingSystemTheme(); |
| return themeProvider->GetNSImageColorNamed(bitmapResources[active][selected]); |
| } |
| |
| // Draws the tab background. |
| - (void)drawFill:(NSRect)dirtyRect { |
| gfx::ScopedNSGraphicsContextSaveGState scopedGState; |
| NSRect bounds = [self bounds]; |
| |
| NSRect clippingRect = bounds; |
| clippingRect.size.height = [TabView maskImageFillHeight]; |
| if (state_ != NSOnState) { |
| // Background tabs should not paint over the tab strip separator, which is |
| // two pixels high in both lodpi and hidpi, and one pixel high in MD. |
| CGFloat tabStripSeparatorLineWidth = [self cr_lineWidth]; |
| clippingRect.origin.y = tabStripSeparatorLineWidth; |
| clippingRect.size.height -= tabStripSeparatorLineWidth; |
| } |
| NSRectClip(clippingRect); |
| |
| NSPoint position = [[self window] |
| themeImagePositionForAlignment:THEME_IMAGE_ALIGN_WITH_TAB_STRIP]; |
| [[NSGraphicsContext currentContext] cr_setPatternPhase:position forView:self]; |
| |
| [[self backgroundColorForSelected:(state_ != NSOffState)] set]; |
| NSRectFill(dirtyRect); |
| |
| if (state_ == NSOffState) |
| [self drawGlow:dirtyRect]; |
| |
| // If we filled outside the middle rect, we need to erase what we filled |
| // outside the tab's shape. |
| // This only works if we are drawing to our own backing layer. |
| if (!NSContainsRect(GetMaskImage().GetMiddleRect(bounds), dirtyRect)) { |
| DCHECK([self layer]); |
| GetMaskImage().DrawInRect(bounds, NSCompositeDestinationIn, 1.0); |
| } |
| } |
| |
| // Draw the glow for hover and the overlay for alerts. |
| - (void)drawGlow:(NSRect)dirtyRect { |
| NSGraphicsContext* context = [NSGraphicsContext currentContext]; |
| CGContextRef cgContext = static_cast<CGContextRef>([context graphicsPort]); |
| |
| CGFloat hoverAlpha = [self hoverAlpha]; |
| CGFloat alertAlpha = [self alertAlpha]; |
| if (hoverAlpha > 0 || alertAlpha > 0) { |
| CGContextBeginTransparencyLayer(cgContext, 0); |
| |
| // The alert glow overlay is like the selected state but at most 80% |
| // opaque. The hover glow brings up the overlay's opacity at most 50%. |
| CGFloat backgroundAlpha = 0.8 * alertAlpha; |
| backgroundAlpha += (1 - backgroundAlpha) * 0.5 * hoverAlpha; |
| CGContextSetAlpha(cgContext, backgroundAlpha); |
| |
| [[self backgroundColorForSelected:YES] set]; |
| NSRectFill(dirtyRect); |
| |
| // ui::ThemeProvider::HasCustomImage is true only if the theme provides the |
| // image. However, even if the theme doesn't provide a tab background, the |
| // theme machinery will make one if given a frame image. See |
| // BrowserThemePack::GenerateTabBackgroundImages for details. |
| const ui::ThemeProvider* themeProvider = [[self window] themeProvider]; |
| BOOL hasCustomTheme = themeProvider && |
| (themeProvider->HasCustomImage(IDR_THEME_TAB_BACKGROUND) || |
| themeProvider->HasCustomImage(IDR_THEME_FRAME)); |
| // Draw a mouse hover gradient for the default themes. |
| if (hoverAlpha > 0) { |
| if (themeProvider && !hasCustomTheme) { |
| CGFloat whiteValue = kMouseHoverWhiteValue; |
| if (themeProvider && themeProvider->InIncognitoMode()) { |
| whiteValue = kMouseHoverWhiteValueIncongito; |
| } |
| base::scoped_nsobject<NSGradient> glow([NSGradient alloc]); |
| [glow initWithStartingColor:[NSColor colorWithCalibratedWhite:whiteValue |
| alpha:1.0 * hoverAlpha] |
| endingColor:[NSColor colorWithCalibratedWhite:whiteValue |
| alpha:0.0]]; |
| NSRect rect = [self bounds]; |
| NSPoint point = hoverPoint_; |
| point.y = NSHeight(rect); |
| [glow drawFromCenter:point |
| radius:0.0 |
| toCenter:point |
| radius:NSWidth(rect) / 3.0 |
| options:NSGradientDrawsBeforeStartingLocation]; |
| } |
| } |
| |
| CGContextEndTransparencyLayer(cgContext); |
| } |
| } |
| |
| // Draws the tab outline. |
| - (void)drawStroke:(NSRect)dirtyRect { |
| // In MD, the tab stroke is always opaque. |
| CGFloat alpha = 1; |
| NSRect bounds = [self bounds]; |
| // In Material Design the tab strip separator is always 1 pixel high - |
| // add a clip rect to avoid drawing the tab edge over it. |
| NSRect clipRect = bounds; |
| clipRect.origin.y += [self cr_lineWidth]; |
| NSRectClip(clipRect); |
| const ui::ThemeProvider* provider = [[self window] themeProvider]; |
| GetStrokeImage(state_ == NSOnState, |
| provider && provider->ShouldIncreaseContrast() |
| ? STROKE_HEAVY |
| : STROKE_NORMAL) |
| .DrawInRect(bounds, NSCompositeSourceOver, alpha); |
| } |
| |
| - (void)drawRect:(NSRect)dirtyRect { |
| [self drawFill:dirtyRect]; |
| [self drawStroke:dirtyRect]; |
| |
| // We draw the title string directly instead of using a NSTextField subview. |
| // This is so that we can get font smoothing to work on earlier OS, and even |
| // when the tab background is a pattern image (when using themes). |
| if (![titleView_ isHidden]) { |
| gfx::ScopedNSGraphicsContextSaveGState scopedGState; |
| NSGraphicsContext* context = [NSGraphicsContext currentContext]; |
| CGContextRef cgContext = static_cast<CGContextRef>([context graphicsPort]); |
| CGContextSetShouldSmoothFonts(cgContext, true); |
| [[titleView_ cell] drawWithFrame:[titleView_ frame] inView:self]; |
| } |
| } |
| |
| - (void)setFrameOrigin:(NSPoint)origin { |
| // The background color depends on the view's vertical position. |
| if (NSMinY([self frame]) != origin.y) |
| [self setNeedsDisplay:YES]; |
| [super setFrameOrigin:origin]; |
| } |
| |
| - (void)setToolTipText:(NSString*)string { |
| toolTipText_.reset([string copy]); |
| } |
| |
| - (NSString*)toolTipText { |
| return toolTipText_; |
| } |
| |
| - (void)viewDidMoveToWindow { |
| [super viewDidMoveToWindow]; |
| if ([self window]) { |
| [controller_ updateTitleColor]; |
| |
| // The new window may have different main window status. |
| // This happens when the view is moved into a TabWindowOverlayWindow for |
| // tab dragging. |
| [self windowDidChangeActive]; |
| } |
| } |
| |
| - (BOOL)isActiveTab { |
| return [controller_ active]; |
| } |
| |
| - (NSString*)title { |
| return [titleView_ stringValue]; |
| } |
| |
| - (void)setTitle:(NSString*)title { |
| if ([title isEqualToString:[titleView_ stringValue]]) |
| return; |
| |
| [titleView_ setStringValue:title]; |
| [closeButton_ setAccessibilityTitle:title]; |
| |
| base::string16 title16 = base::SysNSStringToUTF16(title); |
| bool isRTL = base::i18n::GetFirstStrongCharacterDirection(title16) == |
| base::i18n::RIGHT_TO_LEFT; |
| titleViewCell_.truncateMode = isRTL ? GTMFadeTruncatingHead |
| : GTMFadeTruncatingTail; |
| |
| [self setNeedsDisplayInRect:[titleView_ frame]]; |
| } |
| |
| - (NSRect)titleFrame { |
| return [titleView_ frame]; |
| } |
| |
| - (void)setTitleFrame:(NSRect)titleFrame { |
| NSRect oldTitleFrame = [titleView_ frame]; |
| if (NSEqualRects(titleFrame, oldTitleFrame)) |
| return; |
| [titleView_ setFrame:titleFrame]; |
| [self setNeedsDisplayInRect:NSUnionRect(titleFrame, oldTitleFrame)]; |
| } |
| |
| - (NSColor*)titleColor { |
| return [titleView_ textColor]; |
| } |
| |
| - (void)setTitleColor:(NSColor*)titleColor { |
| if ([titleColor isEqual:[titleView_ textColor]]) |
| return; |
| [titleView_ setTextColor:titleColor]; |
| [self setNeedsDisplayInRect:[titleView_ frame]]; |
| } |
| |
| - (BOOL)titleHidden { |
| return [titleView_ isHidden]; |
| } |
| |
| - (void)setTitleHidden:(BOOL)titleHidden { |
| if (titleHidden == [titleView_ isHidden]) |
| return; |
| [titleView_ setHidden:titleHidden]; |
| [self setNeedsDisplayInRect:[titleView_ frame]]; |
| } |
| |
| - (SkColor)iconColor { |
| if ([[self window] hasDarkTheme]) |
| return kDarkModeIconColor; |
| |
| const ui::ThemeProvider* themeProvider = [[self window] themeProvider]; |
| if (themeProvider) { |
| bool useActiveTabTextColor = [self isActiveTab]; |
| |
| const SkColor titleColor = |
| useActiveTabTextColor |
| ? themeProvider->GetColor(ThemeProperties::COLOR_TAB_TEXT) |
| : themeProvider->GetColor( |
| ThemeProperties::COLOR_BACKGROUND_TAB_TEXT); |
| return SkColorSetA(titleColor, 0xA0); |
| } |
| |
| return tabs::kDefaultTabTextColor; |
| } |
| |
| - (void)accessibilityOptionsDidChange:(id)ignored { |
| [self updateLabelFont]; |
| [self setNeedsDisplay:YES]; |
| } |
| |
| - (void)updateLabelFont { |
| CGFloat fontSize = [titleViewCell_ font].pointSize; |
| const ui::ThemeProvider* provider = [[self window] themeProvider]; |
| if (provider && provider->ShouldIncreaseContrast() && state_ == NSOnState) { |
| [titleViewCell_ setFont:[NSFont boldSystemFontOfSize:fontSize]]; |
| } else { |
| [titleViewCell_ setFont:[NSFont systemFontOfSize:fontSize]]; |
| } |
| } |
| |
| - (void)setState:(NSCellStateValue)state { |
| if (state_ == state) |
| return; |
| state_ = state; |
| [self updateLabelFont]; |
| [self setNeedsDisplay:YES]; |
| [closeButton_ setNeedsDisplay:YES]; |
| } |
| |
| - (void)setClosing:(BOOL)closing { |
| closing_ = closing; // Safe because the property is nonatomic. |
| // When closing, ensure clicks to the close button go nowhere. |
| if (closing) { |
| [closeButton_ setTarget:nil]; |
| [closeButton_ setAction:nil]; |
| } |
| } |
| |
| - (void)startAlert { |
| // Do not start a new alert while already alerting or while in a decay cycle. |
| if (alertState_ == tabs::kAlertNone) { |
| alertState_ = tabs::kAlertRising; |
| [self resetLastGlowUpdateTime]; |
| [self adjustGlowValue]; |
| } |
| } |
| |
| - (void)cancelAlert { |
| if (alertState_ != tabs::kAlertNone) { |
| alertState_ = tabs::kAlertFalling; |
| alertHoldEndTime_ = |
| [NSDate timeIntervalSinceReferenceDate] + kGlowUpdateInterval; |
| [self resetLastGlowUpdateTime]; |
| [self adjustGlowValue]; |
| } |
| } |
| |
| - (int)widthOfLargestSelectableRegion { |
| // Assume the entire region to the left of the alert indicator and/or close |
| // buttons is available for click-to-select. If neither are visible, the |
| // entire tab region is available. |
| AlertIndicatorButton* const indicator = [controller_ alertIndicatorButton]; |
| const int indicatorLeft = (!indicator || [indicator isHidden]) ? |
| NSWidth([self frame]) : NSMinX([indicator frame]); |
| HoverCloseButton* const closeButton = [controller_ closeButton]; |
| const int closeButtonLeft = (!closeButton || [closeButton isHidden]) ? |
| NSWidth([self frame]) : NSMinX([closeButton frame]); |
| return std::min(indicatorLeft, closeButtonLeft); |
| } |
| |
| - (BOOL)accessibilityIsIgnored { |
| return NO; |
| } |
| |
| - (NSArray*)accessibilityActionNames { |
| NSArray* parentActions = [super accessibilityActionNames]; |
| |
| return [parentActions arrayByAddingObject:NSAccessibilityPressAction]; |
| } |
| |
| - (NSArray*)accessibilityAttributeNames { |
| NSMutableArray* attributes = |
| [[super accessibilityAttributeNames] mutableCopy]; |
| [attributes addObject:NSAccessibilityTitleAttribute]; |
| [attributes addObject:NSAccessibilityEnabledAttribute]; |
| [attributes addObject:NSAccessibilityValueAttribute]; |
| |
| return [attributes autorelease]; |
| } |
| |
| - (BOOL)accessibilityIsAttributeSettable:(NSString*)attribute { |
| if ([attribute isEqual:NSAccessibilityTitleAttribute]) |
| return NO; |
| |
| if ([attribute isEqual:NSAccessibilityEnabledAttribute]) |
| return NO; |
| |
| if ([attribute isEqual:NSAccessibilityValueAttribute]) |
| return YES; |
| |
| return [super accessibilityIsAttributeSettable:attribute]; |
| } |
| |
| - (void)accessibilityPerformAction:(NSString*)action { |
| if ([action isEqual:NSAccessibilityPressAction] && |
| [[controller_ target] respondsToSelector:[controller_ action]]) { |
| [[controller_ target] performSelector:[controller_ action] |
| withObject:self]; |
| NSAccessibilityPostNotification(self, |
| NSAccessibilityValueChangedNotification); |
| } else { |
| [super accessibilityPerformAction:action]; |
| } |
| } |
| |
| - (id)accessibilityAttributeValue:(NSString*)attribute { |
| if ([attribute isEqual:NSAccessibilityRoleAttribute]) |
| return NSAccessibilityRadioButtonRole; |
| if ([attribute isEqual:NSAccessibilityRoleDescriptionAttribute]) |
| return l10n_util::GetNSStringWithFixup(IDS_ACCNAME_TAB); |
| if ([attribute isEqual:NSAccessibilityTitleAttribute]) |
| return [controller_ title]; |
| if ([attribute isEqual:NSAccessibilityValueAttribute]) |
| return [NSNumber numberWithInt:[controller_ selected]]; |
| if ([attribute isEqual:NSAccessibilityEnabledAttribute]) |
| return [NSNumber numberWithBool:YES]; |
| |
| return [super accessibilityAttributeValue:attribute]; |
| } |
| |
| - (ViewID)viewID { |
| return VIEW_ID_TAB; |
| } |
| |
| // ThemedWindowDrawing implementation. |
| |
| - (void)windowDidChangeTheme { |
| [self setNeedsDisplay:YES]; |
| } |
| |
| - (void)windowDidChangeActive { |
| [self setNeedsDisplay:YES]; |
| } |
| |
| @end // @implementation TabView |
| |
| @implementation TabView (TabControllerInterface) |
| |
| - (void)setController:(TabController*)controller { |
| controller_ = controller; |
| } |
| |
| @end // @implementation TabView (TabControllerInterface) |
| |
| @implementation TabView(Private) |
| |
| - (void)resetLastGlowUpdateTime { |
| lastGlowUpdate_ = [NSDate timeIntervalSinceReferenceDate]; |
| } |
| |
| - (NSTimeInterval)timeElapsedSinceLastGlowUpdate { |
| return [NSDate timeIntervalSinceReferenceDate] - lastGlowUpdate_; |
| } |
| |
| - (void)adjustGlowValue { |
| // A time interval long enough to represent no update. |
| const NSTimeInterval kNoUpdate = 1000000; |
| |
| // Time until next update for either glow. |
| NSTimeInterval nextUpdate = kNoUpdate; |
| |
| NSTimeInterval elapsed = [self timeElapsedSinceLastGlowUpdate]; |
| NSTimeInterval currentTime = [NSDate timeIntervalSinceReferenceDate]; |
| |
| // TODO(viettrungluu): <http://crbug.com/30617> -- split off the stuff below |
| // into a pure function and add a unit test. |
| |
| CGFloat hoverAlpha = [self hoverAlpha]; |
| if (isMouseInside_) { |
| // Increase hover glow until it's 1. |
| if (hoverAlpha < 1) { |
| hoverAlpha = MIN(hoverAlpha + elapsed / kHoverShowDuration, 1); |
| [self setHoverAlpha:hoverAlpha]; |
| nextUpdate = MIN(kGlowUpdateInterval, nextUpdate); |
| } // Else already 1 (no update needed). |
| } else { |
| if (currentTime >= hoverHoldEndTime_) { |
| // No longer holding, so decrease hover glow until it's 0. |
| if (hoverAlpha > 0) { |
| hoverAlpha = MAX(hoverAlpha - elapsed / kHoverHideDuration, 0); |
| [self setHoverAlpha:hoverAlpha]; |
| nextUpdate = MIN(kGlowUpdateInterval, nextUpdate); |
| } // Else already 0 (no update needed). |
| } else { |
| // Schedule update for end of hold time. |
| nextUpdate = MIN(hoverHoldEndTime_ - currentTime, nextUpdate); |
| } |
| } |
| |
| CGFloat alertAlpha = [self alertAlpha]; |
| if (alertState_ == tabs::kAlertRising) { |
| // Increase alert glow until it's 1 ... |
| alertAlpha = MIN(alertAlpha + elapsed / kAlertShowDuration, 1); |
| [self setAlertAlpha:alertAlpha]; |
| |
| // ... and having reached 1, switch to holding. |
| if (alertAlpha >= 1) { |
| alertState_ = tabs::kAlertHolding; |
| alertHoldEndTime_ = currentTime + kAlertHoldDuration; |
| nextUpdate = MIN(kAlertHoldDuration, nextUpdate); |
| } else { |
| nextUpdate = MIN(kGlowUpdateInterval, nextUpdate); |
| } |
| } else if (alertState_ != tabs::kAlertNone) { |
| if (alertAlpha > 0) { |
| if (currentTime >= alertHoldEndTime_) { |
| // Stop holding, then decrease alert glow (until it's 0). |
| if (alertState_ == tabs::kAlertHolding) { |
| alertState_ = tabs::kAlertFalling; |
| nextUpdate = MIN(kGlowUpdateInterval, nextUpdate); |
| } else { |
| DCHECK_EQ(tabs::kAlertFalling, alertState_); |
| alertAlpha = MAX(alertAlpha - elapsed / kAlertHideDuration, 0); |
| [self setAlertAlpha:alertAlpha]; |
| nextUpdate = MIN(kGlowUpdateInterval, nextUpdate); |
| } |
| } else { |
| // Schedule update for end of hold time. |
| nextUpdate = MIN(alertHoldEndTime_ - currentTime, nextUpdate); |
| } |
| } else { |
| // Done the alert decay cycle. |
| alertState_ = tabs::kAlertNone; |
| } |
| } |
| |
| if (nextUpdate < kNoUpdate) |
| [self performSelector:_cmd withObject:nil afterDelay:nextUpdate]; |
| |
| [self resetLastGlowUpdateTime]; |
| [self setNeedsDisplay:YES]; |
| } |
| |
| @end // @implementation TabView(Private) |
| |
| @implementation TabImageMaker |
| |
| + (NSBezierPath*)tabLeftEdgeBezierPathForContext:(CGContextRef)context { |
| NSBezierPath* bezierPath = [NSBezierPath bezierPath]; |
| |
| [bezierPath moveToPoint:NSMakePoint(-2, 0)]; |
| [bezierPath curveToPoint:NSMakePoint(2.5, 2) |
| controlPoint1:NSMakePoint(1.805, -0.38) |
| controlPoint2:NSMakePoint(2.17, 1.415)]; |
| |
| [bezierPath lineToPoint:NSMakePoint(14, 27)]; |
| [bezierPath curveToPoint:NSMakePoint(16, 29) |
| controlPoint1:NSMakePoint(14.25, 27.25) |
| controlPoint2:NSMakePoint(14.747467, 29.118899)]; |
| |
| [bezierPath lineToPoint:NSMakePoint(18, 29)]; |
| |
| if (!context) { |
| return bezierPath; |
| } |
| |
| // The line width is always 1px. |
| CGFloat lineWidth = LineWidthFromContext(context); |
| [bezierPath setLineWidth:lineWidth]; |
| |
| // Screen pixels lay between integral coordinates in user space. If you draw |
| // a line from (16, 29) to (18, 29), Core Graphics maps that line to the |
| // pixels that lay along y=28.5. In order to achieve a line that appears to |
| // along y=29, CG will perform dithering. To get a crisp line, you have to |
| // specify y=28.5. Translating the bezier path by the 1-pixel line width |
| // creates the crisp line we want. |
| // On a Retina display, there are pixels at y=28.25 and y=28.75, so |
| // translating the path down by one line width lights up the pixels at 28.75 |
| // and leaves a gap along y=28.25. To fix this for the general case we'll |
| // translate up from 28 by one line width. |
| NSAffineTransform* translationTransform = [NSAffineTransform transform]; |
| [translationTransform translateXBy:0 yBy:-1 + lineWidth / 2.]; |
| [bezierPath transformUsingAffineTransform:translationTransform]; |
| |
| return bezierPath; |
| } |
| |
| + (void)setTabEdgeStrokeColor { |
| static NSColor* strokeColor = |
| [skia::SkColorToSRGBNSColor(SkColorSetARGB(76, 0, 0, 0)) retain]; |
| [strokeColor set]; |
| } |
| |
| + (void)drawTabLeftEdgeImage { |
| CGContextRef context = static_cast<CGContextRef>( |
| [[NSGraphicsContext currentContext] graphicsPort]); |
| |
| [self setTabEdgeStrokeColor]; |
| [[self tabLeftEdgeBezierPathForContext:context] stroke]; |
| } |
| |
| + (void)drawTabMiddleEdgeImage { |
| NSBezierPath* middleEdgePath = [NSBezierPath bezierPath]; |
| [middleEdgePath moveToPoint:NSMakePoint(0, 29)]; |
| [middleEdgePath lineToPoint:NSMakePoint(1, 29)]; |
| [middleEdgePath setLineCapStyle:NSSquareLineCapStyle]; |
| |
| CGContextRef context = static_cast<CGContextRef>( |
| [[NSGraphicsContext currentContext] graphicsPort]); |
| CGFloat lineWidth = LineWidthFromContext(context); |
| |
| // Line width is always 1px. |
| [middleEdgePath setLineWidth:lineWidth]; |
| |
| // Align to device pixels. |
| NSAffineTransform* translationTransform = [NSAffineTransform transform]; |
| [translationTransform translateXBy:0 yBy:-1 + lineWidth / 2.]; |
| [middleEdgePath transformUsingAffineTransform:translationTransform]; |
| |
| [self setTabEdgeStrokeColor]; |
| [middleEdgePath stroke]; |
| } |
| |
| + (void)drawTabRightEdgeImage { |
| CGContextRef context = static_cast<CGContextRef>( |
| [[NSGraphicsContext currentContext] graphicsPort]); |
| |
| NSBezierPath* leftEdgePath = [self tabLeftEdgeBezierPathForContext:context]; |
| |
| // Draw the right edge path by flipping the left edge path vertically. |
| NSAffineTransform* transform = [NSAffineTransform transform]; |
| [transform scaleXBy:-1 yBy:1]; |
| [transform translateXBy:-18 yBy:0]; |
| [leftEdgePath transformUsingAffineTransform:transform]; |
| |
| [self setTabEdgeStrokeColor]; |
| [leftEdgePath stroke]; |
| } |
| |
| + (NSBezierPath*)tabLeftMaskBezierPath { |
| NSBezierPath* bezierPath = [self tabLeftEdgeBezierPathForContext:nullptr]; |
| |
| // Box in the open edges. |
| [bezierPath lineToPoint:NSMakePoint(18, 0)]; |
| [bezierPath lineToPoint:NSMakePoint(0, 0)]; |
| |
| [bezierPath closePath]; |
| |
| return bezierPath; |
| } |
| |
| + (void)drawTabLeftMaskImage { |
| NSBezierPath* bezierPath = [self tabLeftMaskBezierPath]; |
| NSAffineTransform* translationTransform = [NSAffineTransform transform]; |
| [translationTransform translateXBy:0.5 yBy:-0.25]; |
| [bezierPath transformUsingAffineTransform:translationTransform]; |
| |
| [[NSColor whiteColor] set]; |
| [bezierPath fill]; |
| } |
| |
| + (void)drawTabRightMaskImage { |
| // Create the right mask image by flipping the left mask path along the |
| // vettical axis. |
| NSBezierPath* bezierPath = [self tabLeftMaskBezierPath]; |
| NSAffineTransform* transform = [NSAffineTransform transform]; |
| [transform scaleXBy:-1 yBy:1]; |
| [transform translateXBy:-17.5 yBy:-0.25]; |
| [bezierPath transformUsingAffineTransform:transform]; |
| |
| [[NSColor whiteColor] set]; |
| [bezierPath fill]; |
| } |
| |
| @end |
| |
| @implementation TabHeavyImageMaker |
| |
| // For "Increase Contrast" mode, use flat black instead of semitransparent black |
| // for the tab edge stroke. |
| + (void)setTabEdgeStrokeColor { |
| static NSColor* heavyStrokeColor = |
| [skia::SkColorToSRGBNSColor(SK_ColorBLACK) retain]; |
| [heavyStrokeColor set]; |
| } |
| |
| @end |