Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(161)

Side by Side Diff: chrome/browser/ui/cocoa/tab_view.mm

Issue 6362007: [Mac] Organize all tab/tab strip files into chrome/browser/ui/cocoa/tabs/.... (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src/
Patch Set: '' Created 9 years, 11 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « chrome/browser/ui/cocoa/tab_view.h ('k') | chrome/browser/ui/cocoa/tab_view_unittest.mm » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 // Copyright (c) 2010 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/tab_view.h"
6
7 #include "base/logging.h"
8 #import "base/mac/mac_util.h"
9 #include "base/mac/scoped_cftyperef.h"
10 #include "chrome/browser/accessibility/browser_accessibility_state.h"
11 #include "chrome/browser/themes/browser_theme_provider.h"
12 #import "chrome/browser/ui/cocoa/tab_controller.h"
13 #import "chrome/browser/ui/cocoa/tab_window_controller.h"
14 #import "chrome/browser/ui/cocoa/themed_window.h"
15 #import "chrome/browser/ui/cocoa/view_id_util.h"
16 #include "grit/theme_resources.h"
17
18 namespace {
19
20 // Constants for inset and control points for tab shape.
21 const CGFloat kInsetMultiplier = 2.0/3.0;
22 const CGFloat kControlPoint1Multiplier = 1.0/3.0;
23 const CGFloat kControlPoint2Multiplier = 3.0/8.0;
24
25 // The amount of time in seconds during which each type of glow increases, holds
26 // steady, and decreases, respectively.
27 const NSTimeInterval kHoverShowDuration = 0.2;
28 const NSTimeInterval kHoverHoldDuration = 0.02;
29 const NSTimeInterval kHoverHideDuration = 0.4;
30 const NSTimeInterval kAlertShowDuration = 0.4;
31 const NSTimeInterval kAlertHoldDuration = 0.4;
32 const NSTimeInterval kAlertHideDuration = 0.4;
33
34 // The default time interval in seconds between glow updates (when
35 // increasing/decreasing).
36 const NSTimeInterval kGlowUpdateInterval = 0.025;
37
38 const CGFloat kTearDistance = 36.0;
39 const NSTimeInterval kTearDuration = 0.333;
40
41 // This is used to judge whether the mouse has moved during rapid closure; if it
42 // has moved less than the threshold, we want to close the tab.
43 const CGFloat kRapidCloseDist = 2.5;
44
45 } // namespace
46
47 @interface TabView(Private)
48
49 - (void)resetLastGlowUpdateTime;
50 - (NSTimeInterval)timeElapsedSinceLastGlowUpdate;
51 - (void)adjustGlowValue;
52 // TODO(davidben): When we stop supporting 10.5, this can be removed.
53 - (int)getWorkspaceID:(NSWindow*)window useCache:(BOOL)useCache;
54 - (NSBezierPath*)bezierPathForRect:(NSRect)rect;
55
56 @end // TabView(Private)
57
58 @implementation TabView
59
60 @synthesize state = state_;
61 @synthesize hoverAlpha = hoverAlpha_;
62 @synthesize alertAlpha = alertAlpha_;
63 @synthesize closing = closing_;
64
65 - (id)initWithFrame:(NSRect)frame {
66 self = [super initWithFrame:frame];
67 if (self) {
68 [self setShowsDivider:NO];
69 // TODO(alcor): register for theming
70 }
71 return self;
72 }
73
74 - (void)awakeFromNib {
75 [self setShowsDivider:NO];
76
77 // It is desirable for us to remove the close button from the cocoa hierarchy,
78 // so that VoiceOver does not encounter it.
79 // TODO(dtseng): crbug.com/59978.
80 // Retain in case we remove it from its superview.
81 closeButtonRetainer_.reset([closeButton_ retain]);
82 if (BrowserAccessibilityState::GetInstance()->IsAccessibleBrowser()) {
83 // The superview gives up ownership of the closeButton here.
84 [closeButton_ removeFromSuperview];
85 }
86 }
87
88 - (void)dealloc {
89 // Cancel any delayed requests that may still be pending (drags or hover).
90 [NSObject cancelPreviousPerformRequestsWithTarget:self];
91 [super dealloc];
92 }
93
94 // Called to obtain the context menu for when the user hits the right mouse
95 // button (or control-clicks). (Note that -rightMouseDown: is *not* called for
96 // control-click.)
97 - (NSMenu*)menu {
98 if ([self isClosing])
99 return nil;
100
101 // Sheets, being window-modal, should block contextual menus. For some reason
102 // they do not. Disallow them ourselves.
103 if ([[self window] attachedSheet])
104 return nil;
105
106 return [controller_ menu];
107 }
108
109 // Overridden so that mouse clicks come to this view (the parent of the
110 // hierarchy) first. We want to handle clicks and drags in this class and
111 // leave the background button for display purposes only.
112 - (BOOL)acceptsFirstMouse:(NSEvent*)theEvent {
113 return YES;
114 }
115
116 - (void)mouseEntered:(NSEvent*)theEvent {
117 isMouseInside_ = YES;
118 [self resetLastGlowUpdateTime];
119 [self adjustGlowValue];
120 }
121
122 - (void)mouseMoved:(NSEvent*)theEvent {
123 hoverPoint_ = [self convertPoint:[theEvent locationInWindow]
124 fromView:nil];
125 [self setNeedsDisplay:YES];
126 }
127
128 - (void)mouseExited:(NSEvent*)theEvent {
129 isMouseInside_ = NO;
130 hoverHoldEndTime_ =
131 [NSDate timeIntervalSinceReferenceDate] + kHoverHoldDuration;
132 [self resetLastGlowUpdateTime];
133 [self adjustGlowValue];
134 }
135
136 - (void)setTrackingEnabled:(BOOL)enabled {
137 [closeButton_ setTrackingEnabled:enabled];
138 }
139
140 // Determines which view a click in our frame actually hit. It's either this
141 // view or our child close button.
142 - (NSView*)hitTest:(NSPoint)aPoint {
143 NSPoint viewPoint = [self convertPoint:aPoint fromView:[self superview]];
144 NSRect frame = [self frame];
145
146 // Reduce the width of the hit rect slightly to remove the overlap
147 // between adjacent tabs. The drawing code in TabCell has the top
148 // corners of the tab inset by height*2/3, so we inset by half of
149 // that here. This doesn't completely eliminate the overlap, but it
150 // works well enough.
151 NSRect hitRect = NSInsetRect(frame, frame.size.height / 3.0f, 0);
152 if (![closeButton_ isHidden])
153 if (NSPointInRect(viewPoint, [closeButton_ frame])) return closeButton_;
154 if (NSPointInRect(aPoint, hitRect)) return self;
155 return nil;
156 }
157
158 // Returns |YES| if this tab can be torn away into a new window.
159 - (BOOL)canBeDragged {
160 if ([self isClosing])
161 return NO;
162 NSWindowController* controller = [sourceWindow_ windowController];
163 if ([controller isKindOfClass:[TabWindowController class]]) {
164 TabWindowController* realController =
165 static_cast<TabWindowController*>(controller);
166 return [realController isTabDraggable:self];
167 }
168 return YES;
169 }
170
171 // Returns an array of controllers that could be a drop target, ordered front to
172 // back. It has to be of the appropriate class, and visible (obviously). Note
173 // that the window cannot be a target for itself.
174 - (NSArray*)dropTargetsForController:(TabWindowController*)dragController {
175 NSMutableArray* targets = [NSMutableArray array];
176 NSWindow* dragWindow = [dragController window];
177 for (NSWindow* window in [NSApp orderedWindows]) {
178 if (window == dragWindow) continue;
179 if (![window isVisible]) continue;
180 // Skip windows on the wrong space.
181 if ([window respondsToSelector:@selector(isOnActiveSpace)]) {
182 if (![window performSelector:@selector(isOnActiveSpace)])
183 continue;
184 } else {
185 // TODO(davidben): When we stop supporting 10.5, this can be
186 // removed.
187 //
188 // We don't cache the workspace of |dragWindow| because it may
189 // move around spaces.
190 if ([self getWorkspaceID:dragWindow useCache:NO] !=
191 [self getWorkspaceID:window useCache:YES])
192 continue;
193 }
194 NSWindowController* controller = [window windowController];
195 if ([controller isKindOfClass:[TabWindowController class]]) {
196 TabWindowController* realController =
197 static_cast<TabWindowController*>(controller);
198 if ([realController canReceiveFrom:dragController])
199 [targets addObject:controller];
200 }
201 }
202 return targets;
203 }
204
205 // Call to clear out transient weak references we hold during drags.
206 - (void)resetDragControllers {
207 draggedController_ = nil;
208 dragWindow_ = nil;
209 dragOverlay_ = nil;
210 sourceController_ = nil;
211 sourceWindow_ = nil;
212 targetController_ = nil;
213 workspaceIDCache_.clear();
214 }
215
216 // Sets whether the window background should be visible or invisible when
217 // dragging a tab. The background should be invisible when the mouse is over a
218 // potential drop target for the tab (the tab strip). It should be visible when
219 // there's no drop target so the window looks more fully realized and ready to
220 // become a stand-alone window.
221 - (void)setWindowBackgroundVisibility:(BOOL)shouldBeVisible {
222 if (chromeIsVisible_ == shouldBeVisible)
223 return;
224
225 // There appears to be a race-condition in CoreAnimation where if we use
226 // animators to set the alpha values, we can't guarantee that we cancel them.
227 // This has the side effect of sometimes leaving the dragged window
228 // translucent or invisible. As a result, don't animate the alpha change.
229 [[draggedController_ overlayWindow] setAlphaValue:1.0];
230 if (targetController_) {
231 [dragWindow_ setAlphaValue:0.0];
232 [[draggedController_ overlayWindow] setHasShadow:YES];
233 [[targetController_ window] makeMainWindow];
234 } else {
235 [dragWindow_ setAlphaValue:0.5];
236 [[draggedController_ overlayWindow] setHasShadow:NO];
237 [[draggedController_ window] makeMainWindow];
238 }
239 chromeIsVisible_ = shouldBeVisible;
240 }
241
242 // Handle clicks and drags in this button. We get here because we have
243 // overridden acceptsFirstMouse: and the click is within our bounds.
244 - (void)mouseDown:(NSEvent*)theEvent {
245 if ([self isClosing])
246 return;
247
248 NSPoint downLocation = [theEvent locationInWindow];
249
250 // Record the state of the close button here, because selecting the tab will
251 // unhide it.
252 BOOL closeButtonActive = [closeButton_ isHidden] ? NO : YES;
253
254 // During the tab closure animation (in particular, during rapid tab closure),
255 // we may get incorrectly hit with a mouse down. If it should have gone to the
256 // close button, we send it there -- it should then track the mouse, so we
257 // don't have to worry about mouse ups.
258 if (closeButtonActive && [controller_ inRapidClosureMode]) {
259 NSPoint hitLocation = [[self superview] convertPoint:downLocation
260 fromView:nil];
261 if ([self hitTest:hitLocation] == closeButton_) {
262 [closeButton_ mouseDown:theEvent];
263 return;
264 }
265 }
266
267 // Fire the action to select the tab.
268 if ([[controller_ target] respondsToSelector:[controller_ action]])
269 [[controller_ target] performSelector:[controller_ action]
270 withObject:self];
271
272 [self resetDragControllers];
273
274 // Resolve overlay back to original window.
275 sourceWindow_ = [self window];
276 if ([sourceWindow_ isKindOfClass:[NSPanel class]]) {
277 sourceWindow_ = [sourceWindow_ parentWindow];
278 }
279
280 sourceWindowFrame_ = [sourceWindow_ frame];
281 sourceTabFrame_ = [self frame];
282 sourceController_ = [sourceWindow_ windowController];
283 tabWasDragged_ = NO;
284 tearTime_ = 0.0;
285 draggingWithinTabStrip_ = YES;
286 chromeIsVisible_ = NO;
287
288 // If there's more than one potential window to be a drop target, we want to
289 // treat a drag of a tab just like dragging around a tab that's already
290 // detached. Note that unit tests might have |-numberOfTabs| reporting zero
291 // since the model won't be fully hooked up. We need to be prepared for that
292 // and not send them into the "magnetic" codepath.
293 NSArray* targets = [self dropTargetsForController:sourceController_];
294 moveWindowOnDrag_ =
295 ([sourceController_ numberOfTabs] < 2 && ![targets count]) ||
296 ![self canBeDragged] ||
297 ![sourceController_ tabDraggingAllowed];
298 // If we are dragging a tab, a window with a single tab should immediately
299 // snap off and not drag within the tab strip.
300 if (!moveWindowOnDrag_)
301 draggingWithinTabStrip_ = [sourceController_ numberOfTabs] > 1;
302
303 dragOrigin_ = [NSEvent mouseLocation];
304
305 // If the tab gets torn off, the tab controller will be removed from the tab
306 // strip and then deallocated. This will also result in *us* being
307 // deallocated. Both these are bad, so we prevent this by retaining the
308 // controller.
309 scoped_nsobject<TabController> controller([controller_ retain]);
310
311 // Because we move views between windows, we need to handle the event loop
312 // ourselves. Ideally we should use the standard event loop.
313 while (1) {
314 theEvent =
315 [NSApp nextEventMatchingMask:NSLeftMouseUpMask | NSLeftMouseDraggedMask
316 untilDate:[NSDate distantFuture]
317 inMode:NSDefaultRunLoopMode dequeue:YES];
318 NSEventType type = [theEvent type];
319 if (type == NSLeftMouseDragged) {
320 [self mouseDragged:theEvent];
321 } else if (type == NSLeftMouseUp) {
322 NSPoint upLocation = [theEvent locationInWindow];
323 CGFloat dx = upLocation.x - downLocation.x;
324 CGFloat dy = upLocation.y - downLocation.y;
325
326 // During rapid tab closure (mashing tab close buttons), we may get hit
327 // with a mouse down. As long as the mouse up is over the close button,
328 // and the mouse hasn't moved too much, we close the tab.
329 if (closeButtonActive &&
330 (dx*dx + dy*dy) <= kRapidCloseDist*kRapidCloseDist &&
331 [controller inRapidClosureMode]) {
332 NSPoint hitLocation =
333 [[self superview] convertPoint:[theEvent locationInWindow]
334 fromView:nil];
335 if ([self hitTest:hitLocation] == closeButton_) {
336 [controller closeTab:self];
337 break;
338 }
339 }
340
341 [self mouseUp:theEvent];
342 break;
343 } else {
344 // TODO(viettrungluu): [crbug.com/23830] We can receive right-mouse-ups
345 // (and maybe even others?) for reasons I don't understand. So we
346 // explicitly check for both events we're expecting, and log others. We
347 // should figure out what's going on.
348 LOG(WARNING) << "Spurious event received of type " << type << ".";
349 }
350 }
351 }
352
353 - (void)mouseDragged:(NSEvent*)theEvent {
354 // Special-case this to keep the logic below simpler.
355 if (moveWindowOnDrag_) {
356 if ([sourceController_ windowMovementAllowed]) {
357 NSPoint thisPoint = [NSEvent mouseLocation];
358 NSPoint origin = sourceWindowFrame_.origin;
359 origin.x += (thisPoint.x - dragOrigin_.x);
360 origin.y += (thisPoint.y - dragOrigin_.y);
361 [sourceWindow_ setFrameOrigin:NSMakePoint(origin.x, origin.y)];
362 } // else do nothing.
363 return;
364 }
365
366 // First, go through the magnetic drag cycle. We break out of this if
367 // "stretchiness" ever exceeds a set amount.
368 tabWasDragged_ = YES;
369
370 if (draggingWithinTabStrip_) {
371 NSPoint thisPoint = [NSEvent mouseLocation];
372 CGFloat stretchiness = thisPoint.y - dragOrigin_.y;
373 stretchiness = copysign(sqrtf(fabs(stretchiness))/sqrtf(kTearDistance),
374 stretchiness) / 2.0;
375 CGFloat offset = thisPoint.x - dragOrigin_.x;
376 if (fabsf(offset) > 100) stretchiness = 0;
377 [sourceController_ insertPlaceholderForTab:self
378 frame:NSOffsetRect(sourceTabFrame_,
379 offset, 0)
380 yStretchiness:stretchiness];
381 // Check that we haven't pulled the tab too far to start a drag. This
382 // can include either pulling it too far down, or off the side of the tab
383 // strip that would cause it to no longer be fully visible.
384 BOOL stillVisible = [sourceController_ isTabFullyVisible:self];
385 CGFloat tearForce = fabs(thisPoint.y - dragOrigin_.y);
386 if ([sourceController_ tabTearingAllowed] &&
387 (tearForce > kTearDistance || !stillVisible)) {
388 draggingWithinTabStrip_ = NO;
389 // When you finally leave the strip, we treat that as the origin.
390 dragOrigin_.x = thisPoint.x;
391 } else {
392 // Still dragging within the tab strip, wait for the next drag event.
393 return;
394 }
395 }
396
397 // Do not start dragging until the user has "torn" the tab off by
398 // moving more than 3 pixels.
399 NSDate* targetDwellDate = nil; // The date this target was first chosen.
400
401 NSPoint thisPoint = [NSEvent mouseLocation];
402
403 // Iterate over possible targets checking for the one the mouse is in.
404 // If the tab is just in the frame, bring the window forward to make it
405 // easier to drop something there. If it's in the tab strip, set the new
406 // target so that it pops into that window. We can't cache this because we
407 // need the z-order to be correct.
408 NSArray* targets = [self dropTargetsForController:draggedController_];
409 TabWindowController* newTarget = nil;
410 for (TabWindowController* target in targets) {
411 NSRect windowFrame = [[target window] frame];
412 if (NSPointInRect(thisPoint, windowFrame)) {
413 [[target window] orderFront:self];
414 NSRect tabStripFrame = [[target tabStripView] frame];
415 tabStripFrame.origin = [[target window]
416 convertBaseToScreen:tabStripFrame.origin];
417 if (NSPointInRect(thisPoint, tabStripFrame)) {
418 newTarget = target;
419 }
420 break;
421 }
422 }
423
424 // If we're now targeting a new window, re-layout the tabs in the old
425 // target and reset how long we've been hovering over this new one.
426 if (targetController_ != newTarget) {
427 targetDwellDate = [NSDate date];
428 [targetController_ removePlaceholder];
429 targetController_ = newTarget;
430 if (!newTarget) {
431 tearTime_ = [NSDate timeIntervalSinceReferenceDate];
432 tearOrigin_ = [dragWindow_ frame].origin;
433 }
434 }
435
436 // Create or identify the dragged controller.
437 if (!draggedController_) {
438 // Get rid of any placeholder remaining in the original source window.
439 [sourceController_ removePlaceholder];
440
441 // Detach from the current window and put it in a new window. If there are
442 // no more tabs remaining after detaching, the source window is about to
443 // go away (it's been autoreleased) so we need to ensure we don't reference
444 // it any more. In that case the new controller becomes our source
445 // controller.
446 draggedController_ = [sourceController_ detachTabToNewWindow:self];
447 dragWindow_ = [draggedController_ window];
448 [dragWindow_ setAlphaValue:0.0];
449 if (![sourceController_ hasLiveTabs]) {
450 sourceController_ = draggedController_;
451 sourceWindow_ = dragWindow_;
452 }
453
454 // If dragging the tab only moves the current window, do not show overlay
455 // so that sheets stay on top of the window.
456 // Bring the target window to the front and make sure it has a border.
457 [dragWindow_ setLevel:NSFloatingWindowLevel];
458 [dragWindow_ setHasShadow:YES];
459 [dragWindow_ orderFront:nil];
460 [dragWindow_ makeMainWindow];
461 [draggedController_ showOverlay];
462 dragOverlay_ = [draggedController_ overlayWindow];
463 // Force the new tab button to be hidden. We'll reset it on mouse up.
464 [draggedController_ showNewTabButton:NO];
465 tearTime_ = [NSDate timeIntervalSinceReferenceDate];
466 tearOrigin_ = sourceWindowFrame_.origin;
467 }
468
469 // TODO(pinkerton): http://crbug.com/25682 demonstrates a way to get here by
470 // some weird circumstance that doesn't first go through mouseDown:. We
471 // really shouldn't go any farther.
472 if (!draggedController_ || !sourceController_)
473 return;
474
475 // When the user first tears off the window, we want slide the window to
476 // the current mouse location (to reduce the jarring appearance). We do this
477 // by calling ourselves back with additional mouseDragged calls (not actual
478 // events). |tearProgress| is a normalized measure of how far through this
479 // tear "animation" (of length kTearDuration) we are and has values [0..1].
480 // We use sqrt() so the animation is non-linear (slow down near the end
481 // point).
482 NSTimeInterval tearProgress =
483 [NSDate timeIntervalSinceReferenceDate] - tearTime_;
484 tearProgress /= kTearDuration; // Normalize.
485 tearProgress = sqrtf(MAX(MIN(tearProgress, 1.0), 0.0));
486
487 // Move the dragged window to the right place on the screen.
488 NSPoint origin = sourceWindowFrame_.origin;
489 origin.x += (thisPoint.x - dragOrigin_.x);
490 origin.y += (thisPoint.y - dragOrigin_.y);
491
492 if (tearProgress < 1) {
493 // If the tear animation is not complete, call back to ourself with the
494 // same event to animate even if the mouse isn't moving. We need to make
495 // sure these get cancelled in mouseUp:.
496 [NSObject cancelPreviousPerformRequestsWithTarget:self];
497 [self performSelector:@selector(mouseDragged:)
498 withObject:theEvent
499 afterDelay:1.0f/30.0f];
500
501 // Set the current window origin based on how far we've progressed through
502 // the tear animation.
503 origin.x = (1 - tearProgress) * tearOrigin_.x + tearProgress * origin.x;
504 origin.y = (1 - tearProgress) * tearOrigin_.y + tearProgress * origin.y;
505 }
506
507 if (targetController_) {
508 // In order to "snap" two windows of different sizes together at their
509 // toolbar, we can't just use the origin of the target frame. We also have
510 // to take into consideration the difference in height.
511 NSRect targetFrame = [[targetController_ window] frame];
512 NSRect sourceFrame = [dragWindow_ frame];
513 origin.y = NSMinY(targetFrame) +
514 (NSHeight(targetFrame) - NSHeight(sourceFrame));
515 }
516 [dragWindow_ setFrameOrigin:NSMakePoint(origin.x, origin.y)];
517
518 // If we're not hovering over any window, make the window fully
519 // opaque. Otherwise, find where the tab might be dropped and insert
520 // a placeholder so it appears like it's part of that window.
521 if (targetController_) {
522 if (![[targetController_ window] isKeyWindow]) {
523 // && ([targetDwellDate timeIntervalSinceNow] < -REQUIRED_DWELL)) {
524 [[targetController_ window] orderFront:nil];
525 targetDwellDate = nil;
526 }
527
528 // Compute where placeholder should go and insert it into the
529 // destination tab strip.
530 TabView* draggedTabView = (TabView*)[draggedController_ selectedTabView];
531 NSRect tabFrame = [draggedTabView frame];
532 tabFrame.origin = [dragWindow_ convertBaseToScreen:tabFrame.origin];
533 tabFrame.origin = [[targetController_ window]
534 convertScreenToBase:tabFrame.origin];
535 tabFrame = [[targetController_ tabStripView]
536 convertRect:tabFrame fromView:nil];
537 [targetController_ insertPlaceholderForTab:self
538 frame:tabFrame
539 yStretchiness:0];
540 [targetController_ layoutTabs];
541 } else {
542 [dragWindow_ makeKeyAndOrderFront:nil];
543 }
544
545 // Adjust the visibility of the window background. If there is a drop target,
546 // we want to hide the window background so the tab stands out for
547 // positioning. If not, we want to show it so it looks like a new window will
548 // be realized.
549 BOOL chromeShouldBeVisible = targetController_ == nil;
550 [self setWindowBackgroundVisibility:chromeShouldBeVisible];
551 }
552
553 - (void)mouseUp:(NSEvent*)theEvent {
554 // The drag/click is done. If the user dragged the mouse, finalize the drag
555 // and clean up.
556
557 // Special-case this to keep the logic below simpler.
558 if (moveWindowOnDrag_)
559 return;
560
561 // Cancel any delayed -mouseDragged: requests that may still be pending.
562 [NSObject cancelPreviousPerformRequestsWithTarget:self];
563
564 // TODO(pinkerton): http://crbug.com/25682 demonstrates a way to get here by
565 // some weird circumstance that doesn't first go through mouseDown:. We
566 // really shouldn't go any farther.
567 if (!sourceController_)
568 return;
569
570 // We are now free to re-display the new tab button in the window we're
571 // dragging. It will show when the next call to -layoutTabs (which happens
572 // indrectly by several of the calls below, such as removing the placeholder).
573 [draggedController_ showNewTabButton:YES];
574
575 if (draggingWithinTabStrip_) {
576 if (tabWasDragged_) {
577 // Move tab to new location.
578 DCHECK([sourceController_ numberOfTabs]);
579 TabWindowController* dropController = sourceController_;
580 [dropController moveTabView:[dropController selectedTabView]
581 fromController:nil];
582 }
583 } else if (targetController_) {
584 // Move between windows. If |targetController_| is nil, we're not dropping
585 // into any existing window.
586 NSView* draggedTabView = [draggedController_ selectedTabView];
587 [targetController_ moveTabView:draggedTabView
588 fromController:draggedController_];
589 // Force redraw to avoid flashes of old content before returning to event
590 // loop.
591 [[targetController_ window] display];
592 [targetController_ showWindow:nil];
593 [draggedController_ removeOverlay];
594 } else {
595 // Only move the window around on screen. Make sure it's set back to
596 // normal state (fully opaque, has shadow, has key, etc).
597 [draggedController_ removeOverlay];
598 // Don't want to re-show the window if it was closed during the drag.
599 if ([dragWindow_ isVisible]) {
600 [dragWindow_ setAlphaValue:1.0];
601 [dragOverlay_ setHasShadow:NO];
602 [dragWindow_ setHasShadow:YES];
603 [dragWindow_ makeKeyAndOrderFront:nil];
604 }
605 [[draggedController_ window] setLevel:NSNormalWindowLevel];
606 [draggedController_ removePlaceholder];
607 }
608 [sourceController_ removePlaceholder];
609 chromeIsVisible_ = YES;
610
611 [self resetDragControllers];
612 }
613
614 - (void)otherMouseUp:(NSEvent*)theEvent {
615 if ([self isClosing])
616 return;
617
618 // Support middle-click-to-close.
619 if ([theEvent buttonNumber] == 2) {
620 // |-hitTest:| takes a location in the superview's coordinates.
621 NSPoint upLocation =
622 [[self superview] convertPoint:[theEvent locationInWindow]
623 fromView:nil];
624 // If the mouse up occurred in our view or over the close button, then
625 // close.
626 if ([self hitTest:upLocation])
627 [controller_ closeTab:self];
628 }
629 }
630
631 - (void)drawRect:(NSRect)dirtyRect {
632 NSGraphicsContext* context = [NSGraphicsContext currentContext];
633 [context saveGraphicsState];
634
635 BrowserThemeProvider* themeProvider =
636 static_cast<BrowserThemeProvider*>([[self window] themeProvider]);
637 [context setPatternPhase:[[self window] themePatternPhase]];
638
639 NSRect rect = [self bounds];
640 NSBezierPath* path = [self bezierPathForRect:rect];
641
642 BOOL selected = [self state];
643 // Don't draw the window/tab bar background when selected, since the tab
644 // background overlay drawn over it (see below) will be fully opaque.
645 BOOL hasBackgroundImage = NO;
646 if (!selected) {
647 // ThemeProvider::HasCustomImage is true only if the theme provides the
648 // image. However, even if the theme doesn't provide a tab background, the
649 // theme machinery will make one if given a frame image. See
650 // BrowserThemePack::GenerateTabBackgroundImages for details.
651 hasBackgroundImage = themeProvider &&
652 (themeProvider->HasCustomImage(IDR_THEME_TAB_BACKGROUND) ||
653 themeProvider->HasCustomImage(IDR_THEME_FRAME));
654
655 NSColor* backgroundImageColor = hasBackgroundImage ?
656 themeProvider->GetNSImageColorNamed(IDR_THEME_TAB_BACKGROUND, true) :
657 nil;
658
659 if (backgroundImageColor) {
660 [backgroundImageColor set];
661 [path fill];
662 } else {
663 // Use the window's background color rather than |[NSColor
664 // windowBackgroundColor]|, which gets confused by the fullscreen window.
665 // (The result is the same for normal, non-fullscreen windows.)
666 [[[self window] backgroundColor] set];
667 [path fill];
668 [[NSColor colorWithCalibratedWhite:1.0 alpha:0.3] set];
669 [path fill];
670 }
671 }
672
673 [context saveGraphicsState];
674 [path addClip];
675
676 // Use the same overlay for the selected state and for hover and alert glows;
677 // for the selected state, it's fully opaque.
678 CGFloat hoverAlpha = [self hoverAlpha];
679 CGFloat alertAlpha = [self alertAlpha];
680 if (selected || hoverAlpha > 0 || alertAlpha > 0) {
681 // Draw the selected background / glow overlay.
682 [context saveGraphicsState];
683 CGContextRef cgContext = static_cast<CGContextRef>([context graphicsPort]);
684 CGContextBeginTransparencyLayer(cgContext, 0);
685 if (!selected) {
686 // The alert glow overlay is like the selected state but at most at most
687 // 80% opaque. The hover glow brings up the overlay's opacity at most 50%.
688 CGFloat backgroundAlpha = 0.8 * alertAlpha;
689 backgroundAlpha += (1 - backgroundAlpha) * 0.5 * hoverAlpha;
690 CGContextSetAlpha(cgContext, backgroundAlpha);
691 }
692 [path addClip];
693 [context saveGraphicsState];
694 [super drawBackground];
695 [context restoreGraphicsState];
696
697 // Draw a mouse hover gradient for the default themes.
698 if (!selected && hoverAlpha > 0) {
699 if (themeProvider && !hasBackgroundImage) {
700 scoped_nsobject<NSGradient> glow([NSGradient alloc]);
701 [glow initWithStartingColor:[NSColor colorWithCalibratedWhite:1.0
702 alpha:1.0 * hoverAlpha]
703 endingColor:[NSColor colorWithCalibratedWhite:1.0
704 alpha:0.0]];
705
706 NSPoint point = hoverPoint_;
707 point.y = NSHeight(rect);
708 [glow drawFromCenter:point
709 radius:0.0
710 toCenter:point
711 radius:NSWidth(rect) / 3.0
712 options:NSGradientDrawsBeforeStartingLocation];
713
714 [glow drawInBezierPath:path relativeCenterPosition:hoverPoint_];
715 }
716 }
717
718 CGContextEndTransparencyLayer(cgContext);
719 [context restoreGraphicsState];
720 }
721
722 BOOL active = [[self window] isKeyWindow] || [[self window] isMainWindow];
723 CGFloat borderAlpha = selected ? (active ? 0.3 : 0.2) : 0.2;
724 NSColor* borderColor = [NSColor colorWithDeviceWhite:0.0 alpha:borderAlpha];
725 NSColor* highlightColor = themeProvider ? themeProvider->GetNSColor(
726 themeProvider->UsingDefaultTheme() ?
727 BrowserThemeProvider::COLOR_TOOLBAR_BEZEL :
728 BrowserThemeProvider::COLOR_TOOLBAR, true) : nil;
729
730 // Draw the top inner highlight within the currently selected tab if using
731 // the default theme.
732 if (selected && themeProvider && themeProvider->UsingDefaultTheme()) {
733 NSAffineTransform* highlightTransform = [NSAffineTransform transform];
734 [highlightTransform translateXBy:1.0 yBy:-1.0];
735 scoped_nsobject<NSBezierPath> highlightPath([path copy]);
736 [highlightPath transformUsingAffineTransform:highlightTransform];
737 [highlightColor setStroke];
738 [highlightPath setLineWidth:1.0];
739 [highlightPath stroke];
740 highlightTransform = [NSAffineTransform transform];
741 [highlightTransform translateXBy:-2.0 yBy:0.0];
742 [highlightPath transformUsingAffineTransform:highlightTransform];
743 [highlightPath stroke];
744 }
745
746 [context restoreGraphicsState];
747
748 // Draw the top stroke.
749 [context saveGraphicsState];
750 [borderColor set];
751 [path setLineWidth:1.0];
752 [path stroke];
753 [context restoreGraphicsState];
754
755 // Mimic the tab strip's bottom border, which consists of a dark border
756 // and light highlight.
757 if (!selected) {
758 [path addClip];
759 NSRect borderRect = rect;
760 borderRect.origin.y = 1;
761 borderRect.size.height = 1;
762 [borderColor set];
763 NSRectFillUsingOperation(borderRect, NSCompositeSourceOver);
764
765 borderRect.origin.y = 0;
766 [highlightColor set];
767 NSRectFillUsingOperation(borderRect, NSCompositeSourceOver);
768 }
769
770 [context restoreGraphicsState];
771 }
772
773 - (void)viewDidMoveToWindow {
774 [super viewDidMoveToWindow];
775 if ([self window]) {
776 [controller_ updateTitleColor];
777 }
778 }
779
780 - (void)setClosing:(BOOL)closing {
781 closing_ = closing; // Safe because the property is nonatomic.
782 // When closing, ensure clicks to the close button go nowhere.
783 if (closing) {
784 [closeButton_ setTarget:nil];
785 [closeButton_ setAction:nil];
786 }
787 }
788
789 - (void)startAlert {
790 // Do not start a new alert while already alerting or while in a decay cycle.
791 if (alertState_ == tabs::kAlertNone) {
792 alertState_ = tabs::kAlertRising;
793 [self resetLastGlowUpdateTime];
794 [self adjustGlowValue];
795 }
796 }
797
798 - (void)cancelAlert {
799 if (alertState_ != tabs::kAlertNone) {
800 alertState_ = tabs::kAlertFalling;
801 alertHoldEndTime_ =
802 [NSDate timeIntervalSinceReferenceDate] + kGlowUpdateInterval;
803 [self resetLastGlowUpdateTime];
804 [self adjustGlowValue];
805 }
806 }
807
808 - (BOOL)accessibilityIsIgnored {
809 return NO;
810 }
811
812 - (NSArray*)accessibilityActionNames {
813 NSArray* parentActions = [super accessibilityActionNames];
814
815 return [parentActions arrayByAddingObject:NSAccessibilityPressAction];
816 }
817
818 - (NSArray*)accessibilityAttributeNames {
819 NSMutableArray* attributes =
820 [[super accessibilityAttributeNames] mutableCopy];
821 [attributes addObject:NSAccessibilityTitleAttribute];
822 [attributes addObject:NSAccessibilityEnabledAttribute];
823
824 return attributes;
825 }
826
827 - (BOOL)accessibilityIsAttributeSettable:(NSString*)attribute {
828 if ([attribute isEqual:NSAccessibilityTitleAttribute])
829 return NO;
830
831 if ([attribute isEqual:NSAccessibilityEnabledAttribute])
832 return NO;
833
834 return [super accessibilityIsAttributeSettable:attribute];
835 }
836
837 - (id)accessibilityAttributeValue:(NSString*)attribute {
838 if ([attribute isEqual:NSAccessibilityRoleAttribute])
839 return NSAccessibilityButtonRole;
840
841 if ([attribute isEqual:NSAccessibilityTitleAttribute])
842 return [controller_ title];
843
844 if ([attribute isEqual:NSAccessibilityEnabledAttribute])
845 return [NSNumber numberWithBool:YES];
846
847 if ([attribute isEqual:NSAccessibilityChildrenAttribute]) {
848 // The subviews (icon and text) are clutter; filter out everything but
849 // useful controls.
850 NSArray* children = [super accessibilityAttributeValue:attribute];
851 NSMutableArray* okChildren = [NSMutableArray array];
852 for (id child in children) {
853 if ([child isKindOfClass:[NSButtonCell class]])
854 [okChildren addObject:child];
855 }
856
857 return okChildren;
858 }
859
860 return [super accessibilityAttributeValue:attribute];
861 }
862
863 - (ViewID)viewID {
864 return VIEW_ID_TAB;
865 }
866
867 @end // @implementation TabView
868
869 @implementation TabView (TabControllerInterface)
870
871 - (void)setController:(TabController*)controller {
872 controller_ = controller;
873 }
874
875 @end // @implementation TabView (TabControllerInterface)
876
877 @implementation TabView(Private)
878
879 - (void)resetLastGlowUpdateTime {
880 lastGlowUpdate_ = [NSDate timeIntervalSinceReferenceDate];
881 }
882
883 - (NSTimeInterval)timeElapsedSinceLastGlowUpdate {
884 return [NSDate timeIntervalSinceReferenceDate] - lastGlowUpdate_;
885 }
886
887 - (void)adjustGlowValue {
888 // A time interval long enough to represent no update.
889 const NSTimeInterval kNoUpdate = 1000000;
890
891 // Time until next update for either glow.
892 NSTimeInterval nextUpdate = kNoUpdate;
893
894 NSTimeInterval elapsed = [self timeElapsedSinceLastGlowUpdate];
895 NSTimeInterval currentTime = [NSDate timeIntervalSinceReferenceDate];
896
897 // TODO(viettrungluu): <http://crbug.com/30617> -- split off the stuff below
898 // into a pure function and add a unit test.
899
900 CGFloat hoverAlpha = [self hoverAlpha];
901 if (isMouseInside_) {
902 // Increase hover glow until it's 1.
903 if (hoverAlpha < 1) {
904 hoverAlpha = MIN(hoverAlpha + elapsed / kHoverShowDuration, 1);
905 [self setHoverAlpha:hoverAlpha];
906 nextUpdate = MIN(kGlowUpdateInterval, nextUpdate);
907 } // Else already 1 (no update needed).
908 } else {
909 if (currentTime >= hoverHoldEndTime_) {
910 // No longer holding, so decrease hover glow until it's 0.
911 if (hoverAlpha > 0) {
912 hoverAlpha = MAX(hoverAlpha - elapsed / kHoverHideDuration, 0);
913 [self setHoverAlpha:hoverAlpha];
914 nextUpdate = MIN(kGlowUpdateInterval, nextUpdate);
915 } // Else already 0 (no update needed).
916 } else {
917 // Schedule update for end of hold time.
918 nextUpdate = MIN(hoverHoldEndTime_ - currentTime, nextUpdate);
919 }
920 }
921
922 CGFloat alertAlpha = [self alertAlpha];
923 if (alertState_ == tabs::kAlertRising) {
924 // Increase alert glow until it's 1 ...
925 alertAlpha = MIN(alertAlpha + elapsed / kAlertShowDuration, 1);
926 [self setAlertAlpha:alertAlpha];
927
928 // ... and having reached 1, switch to holding.
929 if (alertAlpha >= 1) {
930 alertState_ = tabs::kAlertHolding;
931 alertHoldEndTime_ = currentTime + kAlertHoldDuration;
932 nextUpdate = MIN(kAlertHoldDuration, nextUpdate);
933 } else {
934 nextUpdate = MIN(kGlowUpdateInterval, nextUpdate);
935 }
936 } else if (alertState_ != tabs::kAlertNone) {
937 if (alertAlpha > 0) {
938 if (currentTime >= alertHoldEndTime_) {
939 // Stop holding, then decrease alert glow (until it's 0).
940 if (alertState_ == tabs::kAlertHolding) {
941 alertState_ = tabs::kAlertFalling;
942 nextUpdate = MIN(kGlowUpdateInterval, nextUpdate);
943 } else {
944 DCHECK_EQ(tabs::kAlertFalling, alertState_);
945 alertAlpha = MAX(alertAlpha - elapsed / kAlertHideDuration, 0);
946 [self setAlertAlpha:alertAlpha];
947 nextUpdate = MIN(kGlowUpdateInterval, nextUpdate);
948 }
949 } else {
950 // Schedule update for end of hold time.
951 nextUpdate = MIN(alertHoldEndTime_ - currentTime, nextUpdate);
952 }
953 } else {
954 // Done the alert decay cycle.
955 alertState_ = tabs::kAlertNone;
956 }
957 }
958
959 if (nextUpdate < kNoUpdate)
960 [self performSelector:_cmd withObject:nil afterDelay:nextUpdate];
961
962 [self resetLastGlowUpdateTime];
963 [self setNeedsDisplay:YES];
964 }
965
966 // Returns the workspace id of |window|. If |useCache|, then lookup
967 // and remember the value in |workspaceIDCache_| until the end of the
968 // current drag.
969 - (int)getWorkspaceID:(NSWindow*)window useCache:(BOOL)useCache {
970 CGWindowID windowID = [window windowNumber];
971 if (useCache) {
972 std::map<CGWindowID, int>::iterator iter =
973 workspaceIDCache_.find(windowID);
974 if (iter != workspaceIDCache_.end())
975 return iter->second;
976 }
977
978 int workspace = -1;
979 // It's possible to query in bulk, but probably not necessary.
980 base::mac::ScopedCFTypeRef<CFArrayRef> windowIDs(CFArrayCreate(
981 NULL, reinterpret_cast<const void **>(&windowID), 1, NULL));
982 base::mac::ScopedCFTypeRef<CFArrayRef> descriptions(
983 CGWindowListCreateDescriptionFromArray(windowIDs));
984 DCHECK(CFArrayGetCount(descriptions.get()) <= 1);
985 if (CFArrayGetCount(descriptions.get()) > 0) {
986 CFDictionaryRef dict = static_cast<CFDictionaryRef>(
987 CFArrayGetValueAtIndex(descriptions.get(), 0));
988 DCHECK(CFGetTypeID(dict) == CFDictionaryGetTypeID());
989
990 // Sanity check the ID.
991 CFNumberRef otherIDRef = (CFNumberRef)base::mac::GetValueFromDictionary(
992 dict, kCGWindowNumber, CFNumberGetTypeID());
993 CGWindowID otherID;
994 if (otherIDRef &&
995 CFNumberGetValue(otherIDRef, kCGWindowIDCFNumberType, &otherID) &&
996 otherID == windowID) {
997 // And then get the workspace.
998 CFNumberRef workspaceRef = (CFNumberRef)base::mac::GetValueFromDictionary(
999 dict, kCGWindowWorkspace, CFNumberGetTypeID());
1000 if (!workspaceRef ||
1001 !CFNumberGetValue(workspaceRef, kCFNumberIntType, &workspace)) {
1002 workspace = -1;
1003 }
1004 } else {
1005 NOTREACHED();
1006 }
1007 }
1008 if (useCache) {
1009 workspaceIDCache_[windowID] = workspace;
1010 }
1011 return workspace;
1012 }
1013
1014 // Returns the bezier path used to draw the tab given the bounds to draw it in.
1015 - (NSBezierPath*)bezierPathForRect:(NSRect)rect {
1016 // Outset by 0.5 in order to draw on pixels rather than on borders (which
1017 // would cause blurry pixels). Subtract 1px of height to compensate, otherwise
1018 // clipping will occur.
1019 rect = NSInsetRect(rect, -0.5, -0.5);
1020 rect.size.height -= 1.0;
1021
1022 NSPoint bottomLeft = NSMakePoint(NSMinX(rect), NSMinY(rect) + 2);
1023 NSPoint bottomRight = NSMakePoint(NSMaxX(rect), NSMinY(rect) + 2);
1024 NSPoint topRight =
1025 NSMakePoint(NSMaxX(rect) - kInsetMultiplier * NSHeight(rect),
1026 NSMaxY(rect));
1027 NSPoint topLeft =
1028 NSMakePoint(NSMinX(rect) + kInsetMultiplier * NSHeight(rect),
1029 NSMaxY(rect));
1030
1031 CGFloat baseControlPointOutset = NSHeight(rect) * kControlPoint1Multiplier;
1032 CGFloat bottomControlPointInset = NSHeight(rect) * kControlPoint2Multiplier;
1033
1034 // Outset many of these values by 1 to cause the fill to bleed outside the
1035 // clip area.
1036 NSBezierPath* path = [NSBezierPath bezierPath];
1037 [path moveToPoint:NSMakePoint(bottomLeft.x - 1, bottomLeft.y - 2)];
1038 [path lineToPoint:NSMakePoint(bottomLeft.x - 1, bottomLeft.y)];
1039 [path lineToPoint:bottomLeft];
1040 [path curveToPoint:topLeft
1041 controlPoint1:NSMakePoint(bottomLeft.x + baseControlPointOutset,
1042 bottomLeft.y)
1043 controlPoint2:NSMakePoint(topLeft.x - bottomControlPointInset,
1044 topLeft.y)];
1045 [path lineToPoint:topRight];
1046 [path curveToPoint:bottomRight
1047 controlPoint1:NSMakePoint(topRight.x + bottomControlPointInset,
1048 topRight.y)
1049 controlPoint2:NSMakePoint(bottomRight.x - baseControlPointOutset,
1050 bottomRight.y)];
1051 [path lineToPoint:NSMakePoint(bottomRight.x + 1, bottomRight.y)];
1052 [path lineToPoint:NSMakePoint(bottomRight.x + 1, bottomRight.y - 2)];
1053 return path;
1054 }
1055
1056 @end // @implementation TabView(Private)
OLDNEW
« no previous file with comments | « chrome/browser/ui/cocoa/tab_view.h ('k') | chrome/browser/ui/cocoa/tab_view_unittest.mm » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698