blob: 57c46a5537ff68c8743cd5568170625d7584456f [file] [log] [blame]
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001// Copyright (c) 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#import "chrome/browser/ui/cocoa/tabs/tab_view.h"
6
7#include "base/logging.h"
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +01008#include "base/mac/sdk_forward_declarations.h"
Torne (Richard Coles)58218062012-11-14 11:43:16 +00009#include "chrome/browser/themes/theme_service.h"
10#import "chrome/browser/ui/cocoa/nsview_additions.h"
11#import "chrome/browser/ui/cocoa/tabs/tab_controller.h"
12#import "chrome/browser/ui/cocoa/tabs/tab_window_controller.h"
13#import "chrome/browser/ui/cocoa/themed_window.h"
14#import "chrome/browser/ui/cocoa/view_id_util.h"
15#include "grit/generated_resources.h"
16#include "grit/theme_resources.h"
Torne (Richard Coles)7d4cd472013-06-19 11:58:07 +010017#import "ui/base/cocoa/nsgraphics_context_additions.h"
Torne (Richard Coles)58218062012-11-14 11:43:16 +000018#include "ui/base/l10n/l10n_util.h"
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +000019#include "ui/base/resource/resource_bundle.h"
Torne (Richard Coles)58218062012-11-14 11:43:16 +000020#include "ui/gfx/scoped_ns_graphics_context_save_gstate_mac.h"
21
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +000022
23const int kMaskHeight = 29; // Height of the mask bitmap.
24const int kFillHeight = 25; // Height of the "mask on" part of the mask bitmap.
25
Torne (Richard Coles)58218062012-11-14 11:43:16 +000026// Constants for inset and control points for tab shape.
27const CGFloat kInsetMultiplier = 2.0/3.0;
28const CGFloat kControlPoint1Multiplier = 1.0/3.0;
29const CGFloat kControlPoint2Multiplier = 3.0/8.0;
30
31// The amount of time in seconds during which each type of glow increases, holds
32// steady, and decreases, respectively.
33const NSTimeInterval kHoverShowDuration = 0.2;
34const NSTimeInterval kHoverHoldDuration = 0.02;
35const NSTimeInterval kHoverHideDuration = 0.4;
36const NSTimeInterval kAlertShowDuration = 0.4;
37const NSTimeInterval kAlertHoldDuration = 0.4;
38const NSTimeInterval kAlertHideDuration = 0.4;
39
40// The default time interval in seconds between glow updates (when
41// increasing/decreasing).
42const NSTimeInterval kGlowUpdateInterval = 0.025;
43
44// This is used to judge whether the mouse has moved during rapid closure; if it
45// has moved less than the threshold, we want to close the tab.
46const CGFloat kRapidCloseDist = 2.5;
47
Torne (Richard Coles)58218062012-11-14 11:43:16 +000048@interface TabView(Private)
49
50- (void)resetLastGlowUpdateTime;
51- (NSTimeInterval)timeElapsedSinceLastGlowUpdate;
52- (void)adjustGlowValue;
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +000053- (CGImageRef)tabClippingMask;
Torne (Richard Coles)58218062012-11-14 11:43:16 +000054
55@end // TabView(Private)
56
57@implementation TabView
58
59@synthesize state = state_;
60@synthesize hoverAlpha = hoverAlpha_;
61@synthesize alertAlpha = alertAlpha_;
62@synthesize closing = closing_;
63
64+ (CGFloat)insetMultiplier {
65 return kInsetMultiplier;
66}
67
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +000068- (id)initWithFrame:(NSRect)frame
69 controller:(TabController*)controller
70 closeButton:(HoverCloseButton*)closeButton {
Torne (Richard Coles)58218062012-11-14 11:43:16 +000071 self = [super initWithFrame:frame];
72 if (self) {
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +000073 controller_ = controller;
74 closeButton_ = closeButton;
Torne (Richard Coles)58218062012-11-14 11:43:16 +000075 }
76 return self;
77}
78
Torne (Richard Coles)58218062012-11-14 11:43:16 +000079- (void)dealloc {
80 // Cancel any delayed requests that may still be pending (drags or hover).
81 [NSObject cancelPreviousPerformRequestsWithTarget:self];
82 [super dealloc];
83}
84
85// Called to obtain the context menu for when the user hits the right mouse
86// button (or control-clicks). (Note that -rightMouseDown: is *not* called for
87// control-click.)
88- (NSMenu*)menu {
89 if ([self isClosing])
90 return nil;
91
92 // Sheets, being window-modal, should block contextual menus. For some reason
93 // they do not. Disallow them ourselves.
94 if ([[self window] attachedSheet])
95 return nil;
96
97 return [controller_ menu];
98}
99
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000100- (void)resizeSubviewsWithOldSize:(NSSize)oldBoundsSize {
101 [super resizeSubviewsWithOldSize:oldBoundsSize];
102 // Called when our view is resized. If it gets too small, start by hiding
103 // the close button and only show it if tab is selected. Eventually, hide the
104 // icon as well.
105 [controller_ updateVisibility];
106}
107
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000108// Overridden so that mouse clicks come to this view (the parent of the
109// hierarchy) first. We want to handle clicks and drags in this class and
110// leave the background button for display purposes only.
111- (BOOL)acceptsFirstMouse:(NSEvent*)theEvent {
112 return YES;
113}
114
115- (void)mouseEntered:(NSEvent*)theEvent {
116 isMouseInside_ = YES;
117 [self resetLastGlowUpdateTime];
118 [self adjustGlowValue];
119}
120
121- (void)mouseMoved:(NSEvent*)theEvent {
122 hoverPoint_ = [self convertPoint:[theEvent locationInWindow]
123 fromView:nil];
124 [self setNeedsDisplay:YES];
125}
126
127- (void)mouseExited:(NSEvent*)theEvent {
128 isMouseInside_ = NO;
129 hoverHoldEndTime_ =
130 [NSDate timeIntervalSinceReferenceDate] + kHoverHoldDuration;
131 [self resetLastGlowUpdateTime];
132 [self adjustGlowValue];
133}
134
135- (void)setTrackingEnabled:(BOOL)enabled {
136 if (![closeButton_ isHidden]) {
137 [closeButton_ setTrackingEnabled:enabled];
138 }
139}
140
141// Determines which view a click in our frame actually hit. It's either this
142// view or our child close button.
143- (NSView*)hitTest:(NSPoint)aPoint {
144 NSPoint viewPoint = [self convertPoint:aPoint fromView:[self superview]];
Ben Murdocheb525c52013-07-10 11:40:50 +0100145 if (![closeButton_ isHidden])
146 if (NSPointInRect(viewPoint, [closeButton_ frame])) return closeButton_;
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000147
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000148 NSRect pointRect = NSMakeRect(viewPoint.x, viewPoint.y, 1, 1);
149
150 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
151 NSImage* left = rb.GetNativeImageNamed(IDR_TAB_ALPHA_LEFT).ToNSImage();
152 if (viewPoint.x < [left size].width) {
153 NSRect imageRect = NSMakeRect(0, 0, [left size].width, [left size].height);
154 if ([left hitTestRect:pointRect withImageDestinationRect:imageRect
155 context:nil hints:nil flipped:NO]) {
156 return self;
157 }
158 return nil;
159 }
160
161 NSImage* right = rb.GetNativeImageNamed(IDR_TAB_ALPHA_RIGHT).ToNSImage();
162 CGFloat rightX = NSWidth([self bounds]) - [right size].width;
163 if (viewPoint.x > rightX) {
164 NSRect imageRect = NSMakeRect(
165 rightX, 0, [right size].width, [right size].height);
166 if ([right hitTestRect:pointRect withImageDestinationRect:imageRect
167 context:nil hints:nil flipped:NO]) {
168 return self;
169 }
170 return nil;
171 }
172
173 if (viewPoint.y < kFillHeight)
174 return self;
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000175 return nil;
176}
177
178// Returns |YES| if this tab can be torn away into a new window.
179- (BOOL)canBeDragged {
180 return [controller_ tabCanBeDragged:controller_];
181}
182
183// Handle clicks and drags in this button. We get here because we have
184// overridden acceptsFirstMouse: and the click is within our bounds.
185- (void)mouseDown:(NSEvent*)theEvent {
186 if ([self isClosing])
187 return;
188
189 // Record the point at which this event happened. This is used by other mouse
190 // events that are dispatched from |-maybeStartDrag::|.
191 mouseDownPoint_ = [theEvent locationInWindow];
192
193 // Record the state of the close button here, because selecting the tab will
194 // unhide it.
195 BOOL closeButtonActive = ![closeButton_ isHidden];
196
197 // During the tab closure animation (in particular, during rapid tab closure),
198 // we may get incorrectly hit with a mouse down. If it should have gone to the
199 // close button, we send it there -- it should then track the mouse, so we
200 // don't have to worry about mouse ups.
201 if (closeButtonActive && [controller_ inRapidClosureMode]) {
202 NSPoint hitLocation = [[self superview] convertPoint:mouseDownPoint_
203 fromView:nil];
204 if ([self hitTest:hitLocation] == closeButton_) {
205 [closeButton_ mouseDown:theEvent];
206 return;
207 }
208 }
209
210 // If the tab gets torn off, the tab controller will be removed from the tab
211 // strip and then deallocated. This will also result in *us* being
212 // deallocated. Both these are bad, so we prevent this by retaining the
213 // controller.
Ben Murdocheb525c52013-07-10 11:40:50 +0100214 base::scoped_nsobject<TabController> controller([controller_ retain]);
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000215
216 // Try to initiate a drag. This will spin a custom event loop and may
217 // dispatch other mouse events.
218 [controller_ maybeStartDrag:theEvent forTab:controller];
219
220 // The custom loop has ended, so clear the point.
221 mouseDownPoint_ = NSZeroPoint;
222}
223
224- (void)mouseUp:(NSEvent*)theEvent {
225 // Check for rapid tab closure.
226 if ([theEvent type] == NSLeftMouseUp) {
227 NSPoint upLocation = [theEvent locationInWindow];
228 CGFloat dx = upLocation.x - mouseDownPoint_.x;
229 CGFloat dy = upLocation.y - mouseDownPoint_.y;
230
231 // During rapid tab closure (mashing tab close buttons), we may get hit
232 // with a mouse down. As long as the mouse up is over the close button,
233 // and the mouse hasn't moved too much, we close the tab.
234 if (![closeButton_ isHidden] &&
235 (dx*dx + dy*dy) <= kRapidCloseDist*kRapidCloseDist &&
236 [controller_ inRapidClosureMode]) {
237 NSPoint hitLocation =
238 [[self superview] convertPoint:[theEvent locationInWindow]
239 fromView:nil];
240 if ([self hitTest:hitLocation] == closeButton_) {
241 [controller_ closeTab:self];
242 return;
243 }
244 }
245 }
246
247 // Fire the action to select the tab.
248 [controller_ selectTab:self];
249
250 // Messaging the drag controller with |-endDrag:| would seem like the right
251 // thing to do here. But, when a tab has been detached, the controller's
252 // target is nil until the drag is finalized. Since |-mouseUp:| gets called
253 // via the manual event loop inside -[TabStripDragController
254 // maybeStartDrag:forTab:], the drag controller can end the dragging session
255 // itself directly after calling this.
256}
257
258- (void)otherMouseUp:(NSEvent*)theEvent {
259 if ([self isClosing])
260 return;
261
262 // Support middle-click-to-close.
263 if ([theEvent buttonNumber] == 2) {
264 // |-hitTest:| takes a location in the superview's coordinates.
265 NSPoint upLocation =
266 [[self superview] convertPoint:[theEvent locationInWindow]
267 fromView:nil];
268 // If the mouse up occurred in our view or over the close button, then
269 // close.
270 if ([self hitTest:upLocation])
271 [controller_ closeTab:self];
272 }
273}
274
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000275// Returns the color used to draw the background of a tab. |selected| selects
276// between the foreground and background tabs.
277- (NSColor*)backgroundColorForSelected:(bool)selected {
278 ThemeService* themeProvider =
279 static_cast<ThemeService*>([[self window] themeProvider]);
280 if (!themeProvider)
281 return [[self window] backgroundColor];
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000282
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000283 int bitmapResources[2][2] = {
284 // Background window.
285 {
286 IDR_THEME_TAB_BACKGROUND_INACTIVE, // Background tab.
287 IDR_THEME_TOOLBAR_INACTIVE, // Active tab.
288 },
289 // Currently focused window.
290 {
291 IDR_THEME_TAB_BACKGROUND, // Background tab.
292 IDR_THEME_TOOLBAR, // Active tab.
293 },
294 };
295
296 // Themes don't have an inactive image so only look for one if there's no
297 // theme.
298 bool active = [[self window] isKeyWindow] || [[self window] isMainWindow] ||
299 !themeProvider->UsingDefaultTheme();
Ben Murdochbb1529c2013-08-08 10:24:53 +0100300 return themeProvider->GetNSImageColorNamed(bitmapResources[active][selected]);
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000301}
302
Ben Murdochca12bfa2013-07-23 11:17:05 +0100303// Draws the active tab background.
304- (void)drawFillForActiveTab:(NSRect)dirtyRect {
305 NSColor* backgroundImageColor = [self backgroundColorForSelected:YES];
306 [backgroundImageColor set];
307
308 // Themes can have partially transparent images. NSRectFill() is measurably
309 // faster though, so call it for the known-safe default theme.
310 ThemeService* themeProvider =
311 static_cast<ThemeService*>([[self window] themeProvider]);
312 if (themeProvider && themeProvider->UsingDefaultTheme())
313 NSRectFill(dirtyRect);
314 else
315 NSRectFillUsingOperation(dirtyRect, NSCompositeSourceOver);
316}
317
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000318// Draws the tab background.
319- (void)drawFill:(NSRect)dirtyRect {
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000320 gfx::ScopedNSGraphicsContextSaveGState scopedGState;
321 NSGraphicsContext* context = [NSGraphicsContext currentContext];
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000322 CGContextRef cgContext = static_cast<CGContextRef>([context graphicsPort]);
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000323
324 ThemeService* themeProvider =
325 static_cast<ThemeService*>([[self window] themeProvider]);
Ben Murdochca12bfa2013-07-23 11:17:05 +0100326 NSPoint phase = [[self window]
327 themePatternPhaseForAlignment: THEME_PATTERN_ALIGN_WITH_TAB_STRIP];
328 [context cr_setPatternPhase:phase forView:self];
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000329
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000330 CGImageRef mask([self tabClippingMask]);
331 CGRect maskBounds = CGRectMake(0, 0, maskCacheWidth_, kMaskHeight);
332 CGContextClipToMask(cgContext, maskBounds, mask);
333
334 bool selected = [self state];
Ben Murdochca12bfa2013-07-23 11:17:05 +0100335 if (selected) {
336 [self drawFillForActiveTab:dirtyRect];
337 return;
338 }
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000339
340 // Background tabs should not paint over the tab strip separator, which is
341 // two pixels high in both lodpi and hidpi.
Ben Murdochca12bfa2013-07-23 11:17:05 +0100342 if (dirtyRect.origin.y < 1)
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000343 dirtyRect.origin.y = 2 * [self cr_lineWidth];
344
Ben Murdochca12bfa2013-07-23 11:17:05 +0100345 // Draw the tab background.
346 NSColor* backgroundImageColor = [self backgroundColorForSelected:NO];
347 [backgroundImageColor set];
348
349 // Themes can have partially transparent images. NSRectFill() is measurably
350 // faster though, so call it for the known-safe default theme.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000351 bool usingDefaultTheme = themeProvider && themeProvider->UsingDefaultTheme();
Ben Murdochca12bfa2013-07-23 11:17:05 +0100352 if (usingDefaultTheme)
353 NSRectFill(dirtyRect);
354 else
355 NSRectFillUsingOperation(dirtyRect, NSCompositeSourceOver);
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000356
Ben Murdochca12bfa2013-07-23 11:17:05 +0100357 // Draw the glow for hover and the overlay for alerts.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000358 CGFloat hoverAlpha = [self hoverAlpha];
359 CGFloat alertAlpha = [self alertAlpha];
Ben Murdochca12bfa2013-07-23 11:17:05 +0100360 if (hoverAlpha > 0 || alertAlpha > 0) {
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000361 gfx::ScopedNSGraphicsContextSaveGState contextSave;
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000362 CGContextBeginTransparencyLayer(cgContext, 0);
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000363
Ben Murdochca12bfa2013-07-23 11:17:05 +0100364 // The alert glow overlay is like the selected state but at most at most 80%
365 // opaque. The hover glow brings up the overlay's opacity at most 50%.
366 CGFloat backgroundAlpha = 0.8 * alertAlpha;
367 backgroundAlpha += (1 - backgroundAlpha) * 0.5 * hoverAlpha;
368 CGContextSetAlpha(cgContext, backgroundAlpha);
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000369
Ben Murdochca12bfa2013-07-23 11:17:05 +0100370 [self drawFillForActiveTab:dirtyRect];
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000371
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000372 // ui::ThemeProvider::HasCustomImage is true only if the theme provides the
373 // image. However, even if the theme doesn't provide a tab background, the
374 // theme machinery will make one if given a frame image. See
375 // BrowserThemePack::GenerateTabBackgroundImages for details.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000376 BOOL hasCustomTheme = themeProvider &&
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000377 (themeProvider->HasCustomImage(IDR_THEME_TAB_BACKGROUND) ||
378 themeProvider->HasCustomImage(IDR_THEME_FRAME));
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000379 // Draw a mouse hover gradient for the default themes.
Ben Murdochca12bfa2013-07-23 11:17:05 +0100380 if (hoverAlpha > 0) {
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000381 if (themeProvider && !hasCustomTheme) {
Ben Murdocheb525c52013-07-10 11:40:50 +0100382 base::scoped_nsobject<NSGradient> glow([NSGradient alloc]);
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000383 [glow initWithStartingColor:[NSColor colorWithCalibratedWhite:1.0
384 alpha:1.0 * hoverAlpha]
385 endingColor:[NSColor colorWithCalibratedWhite:1.0
386 alpha:0.0]];
387 NSRect rect = [self bounds];
388 NSPoint point = hoverPoint_;
389 point.y = NSHeight(rect);
390 [glow drawFromCenter:point
391 radius:0.0
392 toCenter:point
393 radius:NSWidth(rect) / 3.0
394 options:NSGradientDrawsBeforeStartingLocation];
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000395 }
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000396 }
397
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000398 CGContextEndTransparencyLayer(cgContext);
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000399 }
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000400}
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000401
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000402// Draws the tab outline.
403- (void)drawStroke:(NSRect)dirtyRect {
404 BOOL focused = [[self window] isKeyWindow] || [[self window] isMainWindow];
405 CGFloat alpha = focused ? 1.0 : tabs::kImageNoFocusAlpha;
406
407 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
408 float height =
409 [rb.GetNativeImageNamed(IDR_TAB_ACTIVE_LEFT).ToNSImage() size].height;
410 if ([controller_ active]) {
411 NSDrawThreePartImage(NSMakeRect(0, 0, NSWidth([self bounds]), height),
412 rb.GetNativeImageNamed(IDR_TAB_ACTIVE_LEFT).ToNSImage(),
413 rb.GetNativeImageNamed(IDR_TAB_ACTIVE_CENTER).ToNSImage(),
414 rb.GetNativeImageNamed(IDR_TAB_ACTIVE_RIGHT).ToNSImage(),
415 /*vertical=*/NO,
416 NSCompositeSourceOver,
417 alpha,
418 /*flipped=*/NO);
419 } else {
420 NSDrawThreePartImage(NSMakeRect(0, 0, NSWidth([self bounds]), height),
421 rb.GetNativeImageNamed(IDR_TAB_INACTIVE_LEFT).ToNSImage(),
422 rb.GetNativeImageNamed(IDR_TAB_INACTIVE_CENTER).ToNSImage(),
423 rb.GetNativeImageNamed(IDR_TAB_INACTIVE_RIGHT).ToNSImage(),
424 /*vertical=*/NO,
425 NSCompositeSourceOver,
426 alpha,
427 /*flipped=*/NO);
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000428 }
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000429}
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000430
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000431- (void)drawRect:(NSRect)dirtyRect {
432 // Text, close button, and image are drawn by subviews.
433 [self drawFill:dirtyRect];
434 [self drawStroke:dirtyRect];
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000435}
436
Ben Murdocheb525c52013-07-10 11:40:50 +0100437- (void)setFrameOrigin:(NSPoint)origin {
438 // The background color depends on the view's vertical position.
439 if (NSMinY([self frame]) != origin.y)
440 [self setNeedsDisplay:YES];
441 [super setFrameOrigin:origin];
442}
443
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000444// Override this to catch the text so that we can choose when to display it.
445- (void)setToolTip:(NSString*)string {
446 toolTipText_.reset([string retain]);
447}
448
449- (NSString*)toolTipText {
450 if (!toolTipText_.get()) {
451 return @"";
452 }
453 return toolTipText_.get();
454}
455
456- (void)viewDidMoveToWindow {
457 [super viewDidMoveToWindow];
458 if ([self window]) {
459 [controller_ updateTitleColor];
460 }
461}
462
Ben Murdocheb525c52013-07-10 11:40:50 +0100463- (void)setState:(NSCellStateValue)state {
464 if (state_ == state)
465 return;
466 state_ = state;
467 [self setNeedsDisplay:YES];
468}
469
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000470- (void)setClosing:(BOOL)closing {
471 closing_ = closing; // Safe because the property is nonatomic.
472 // When closing, ensure clicks to the close button go nowhere.
473 if (closing) {
474 [closeButton_ setTarget:nil];
475 [closeButton_ setAction:nil];
476 }
477}
478
479- (void)startAlert {
480 // Do not start a new alert while already alerting or while in a decay cycle.
481 if (alertState_ == tabs::kAlertNone) {
482 alertState_ = tabs::kAlertRising;
483 [self resetLastGlowUpdateTime];
484 [self adjustGlowValue];
485 }
486}
487
488- (void)cancelAlert {
489 if (alertState_ != tabs::kAlertNone) {
490 alertState_ = tabs::kAlertFalling;
491 alertHoldEndTime_ =
492 [NSDate timeIntervalSinceReferenceDate] + kGlowUpdateInterval;
493 [self resetLastGlowUpdateTime];
494 [self adjustGlowValue];
495 }
496}
497
498- (BOOL)accessibilityIsIgnored {
499 return NO;
500}
501
502- (NSArray*)accessibilityActionNames {
503 NSArray* parentActions = [super accessibilityActionNames];
504
505 return [parentActions arrayByAddingObject:NSAccessibilityPressAction];
506}
507
508- (NSArray*)accessibilityAttributeNames {
509 NSMutableArray* attributes =
510 [[super accessibilityAttributeNames] mutableCopy];
511 [attributes addObject:NSAccessibilityTitleAttribute];
512 [attributes addObject:NSAccessibilityEnabledAttribute];
513 [attributes addObject:NSAccessibilityValueAttribute];
514
515 return [attributes autorelease];
516}
517
518- (BOOL)accessibilityIsAttributeSettable:(NSString*)attribute {
519 if ([attribute isEqual:NSAccessibilityTitleAttribute])
520 return NO;
521
522 if ([attribute isEqual:NSAccessibilityEnabledAttribute])
523 return NO;
524
525 if ([attribute isEqual:NSAccessibilityValueAttribute])
526 return YES;
527
528 return [super accessibilityIsAttributeSettable:attribute];
529}
530
531- (void)accessibilityPerformAction:(NSString*)action {
532 if ([action isEqual:NSAccessibilityPressAction] &&
533 [[controller_ target] respondsToSelector:[controller_ action]]) {
534 [[controller_ target] performSelector:[controller_ action]
535 withObject:self];
536 NSAccessibilityPostNotification(self,
537 NSAccessibilityValueChangedNotification);
538 } else {
539 [super accessibilityPerformAction:action];
540 }
541}
542
543- (id)accessibilityAttributeValue:(NSString*)attribute {
544 if ([attribute isEqual:NSAccessibilityRoleAttribute])
545 return NSAccessibilityRadioButtonRole;
546 if ([attribute isEqual:NSAccessibilityRoleDescriptionAttribute])
547 return l10n_util::GetNSStringWithFixup(IDS_ACCNAME_TAB);
548 if ([attribute isEqual:NSAccessibilityTitleAttribute])
549 return [controller_ title];
550 if ([attribute isEqual:NSAccessibilityValueAttribute])
551 return [NSNumber numberWithInt:[controller_ selected]];
552 if ([attribute isEqual:NSAccessibilityEnabledAttribute])
553 return [NSNumber numberWithBool:YES];
554
555 return [super accessibilityAttributeValue:attribute];
556}
557
558- (ViewID)viewID {
559 return VIEW_ID_TAB;
560}
561
562@end // @implementation TabView
563
564@implementation TabView (TabControllerInterface)
565
566- (void)setController:(TabController*)controller {
567 controller_ = controller;
568}
569
570@end // @implementation TabView (TabControllerInterface)
571
572@implementation TabView(Private)
573
574- (void)resetLastGlowUpdateTime {
575 lastGlowUpdate_ = [NSDate timeIntervalSinceReferenceDate];
576}
577
578- (NSTimeInterval)timeElapsedSinceLastGlowUpdate {
579 return [NSDate timeIntervalSinceReferenceDate] - lastGlowUpdate_;
580}
581
582- (void)adjustGlowValue {
583 // A time interval long enough to represent no update.
584 const NSTimeInterval kNoUpdate = 1000000;
585
586 // Time until next update for either glow.
587 NSTimeInterval nextUpdate = kNoUpdate;
588
589 NSTimeInterval elapsed = [self timeElapsedSinceLastGlowUpdate];
590 NSTimeInterval currentTime = [NSDate timeIntervalSinceReferenceDate];
591
592 // TODO(viettrungluu): <http://crbug.com/30617> -- split off the stuff below
593 // into a pure function and add a unit test.
594
595 CGFloat hoverAlpha = [self hoverAlpha];
596 if (isMouseInside_) {
597 // Increase hover glow until it's 1.
598 if (hoverAlpha < 1) {
599 hoverAlpha = MIN(hoverAlpha + elapsed / kHoverShowDuration, 1);
600 [self setHoverAlpha:hoverAlpha];
601 nextUpdate = MIN(kGlowUpdateInterval, nextUpdate);
602 } // Else already 1 (no update needed).
603 } else {
604 if (currentTime >= hoverHoldEndTime_) {
605 // No longer holding, so decrease hover glow until it's 0.
606 if (hoverAlpha > 0) {
607 hoverAlpha = MAX(hoverAlpha - elapsed / kHoverHideDuration, 0);
608 [self setHoverAlpha:hoverAlpha];
609 nextUpdate = MIN(kGlowUpdateInterval, nextUpdate);
610 } // Else already 0 (no update needed).
611 } else {
612 // Schedule update for end of hold time.
613 nextUpdate = MIN(hoverHoldEndTime_ - currentTime, nextUpdate);
614 }
615 }
616
617 CGFloat alertAlpha = [self alertAlpha];
618 if (alertState_ == tabs::kAlertRising) {
619 // Increase alert glow until it's 1 ...
620 alertAlpha = MIN(alertAlpha + elapsed / kAlertShowDuration, 1);
621 [self setAlertAlpha:alertAlpha];
622
623 // ... and having reached 1, switch to holding.
624 if (alertAlpha >= 1) {
625 alertState_ = tabs::kAlertHolding;
626 alertHoldEndTime_ = currentTime + kAlertHoldDuration;
627 nextUpdate = MIN(kAlertHoldDuration, nextUpdate);
628 } else {
629 nextUpdate = MIN(kGlowUpdateInterval, nextUpdate);
630 }
631 } else if (alertState_ != tabs::kAlertNone) {
632 if (alertAlpha > 0) {
633 if (currentTime >= alertHoldEndTime_) {
634 // Stop holding, then decrease alert glow (until it's 0).
635 if (alertState_ == tabs::kAlertHolding) {
636 alertState_ = tabs::kAlertFalling;
637 nextUpdate = MIN(kGlowUpdateInterval, nextUpdate);
638 } else {
639 DCHECK_EQ(tabs::kAlertFalling, alertState_);
640 alertAlpha = MAX(alertAlpha - elapsed / kAlertHideDuration, 0);
641 [self setAlertAlpha:alertAlpha];
642 nextUpdate = MIN(kGlowUpdateInterval, nextUpdate);
643 }
644 } else {
645 // Schedule update for end of hold time.
646 nextUpdate = MIN(alertHoldEndTime_ - currentTime, nextUpdate);
647 }
648 } else {
649 // Done the alert decay cycle.
650 alertState_ = tabs::kAlertNone;
651 }
652 }
653
654 if (nextUpdate < kNoUpdate)
655 [self performSelector:_cmd withObject:nil afterDelay:nextUpdate];
656
657 [self resetLastGlowUpdateTime];
658 [self setNeedsDisplay:YES];
659}
660
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000661- (CGImageRef)tabClippingMask {
662 // NOTE: NSHeight([self bounds]) doesn't match the height of the bitmaps.
663 CGFloat scale = 1;
664 if ([[self window] respondsToSelector:@selector(backingScaleFactor)])
665 scale = [[self window] backingScaleFactor];
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000666
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000667 NSRect bounds = [self bounds];
668 CGFloat tabWidth = NSWidth(bounds);
669 if (tabWidth == maskCacheWidth_ && scale == maskCacheScale_)
670 return maskCache_.get();
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000671
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000672 maskCacheWidth_ = tabWidth;
673 maskCacheScale_ = scale;
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000674
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000675 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
676 NSImage* leftMask = rb.GetNativeImageNamed(IDR_TAB_ALPHA_LEFT).ToNSImage();
677 NSImage* rightMask = rb.GetNativeImageNamed(IDR_TAB_ALPHA_RIGHT).ToNSImage();
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000678
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000679 CGFloat leftWidth = leftMask.size.width;
680 CGFloat rightWidth = rightMask.size.width;
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000681
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000682 // Image masks must be in the DeviceGray colorspace. Create a context and
683 // draw the mask into it.
Ben Murdocheb525c52013-07-10 11:40:50 +0100684 base::ScopedCFTypeRef<CGColorSpaceRef> colorspace(
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000685 CGColorSpaceCreateDeviceGray());
686 CGContextRef maskContext =
687 CGBitmapContextCreate(NULL, tabWidth * scale, kMaskHeight * scale,
688 8, tabWidth * scale, colorspace, 0);
689 CGContextScaleCTM(maskContext, scale, scale);
690 NSGraphicsContext* maskGraphicsContext =
691 [NSGraphicsContext graphicsContextWithGraphicsPort:maskContext
692 flipped:NO];
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000693
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000694 gfx::ScopedNSGraphicsContextSaveGState scopedGState;
695 [NSGraphicsContext setCurrentContext:maskGraphicsContext];
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000696
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000697 // Draw mask image.
698 [[NSColor blackColor] setFill];
699 CGContextFillRect(maskContext, CGRectMake(0, 0, tabWidth, kMaskHeight));
700
701 NSDrawThreePartImage(NSMakeRect(0, 0, tabWidth, kMaskHeight),
702 leftMask, nil, rightMask, /*vertical=*/NO, NSCompositeSourceOver, 1.0,
703 /*flipped=*/NO);
704
705 CGFloat middleWidth = tabWidth - leftWidth - rightWidth;
706 NSRect middleRect = NSMakeRect(leftWidth, 0, middleWidth, kFillHeight);
707 [[NSColor whiteColor] setFill];
708 NSRectFill(middleRect);
709
710 maskCache_.reset(CGBitmapContextCreateImage(maskContext));
711 return maskCache_;
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000712}
713
714@end // @implementation TabView(Private)