Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 1 | // 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_strip_controller.h" |
| 6 | |
| 7 | #import <QuartzCore/QuartzCore.h> |
| 8 | |
| 9 | #include <cmath> |
| 10 | #include <limits> |
| 11 | #include <string> |
| 12 | |
| 13 | #include "base/command_line.h" |
| 14 | #include "base/mac/mac_util.h" |
| 15 | #include "base/mac/scoped_nsautorelease_pool.h" |
| 16 | #include "base/metrics/histogram.h" |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 17 | #include "base/prefs/pref_service.h" |
Torne (Richard Coles) | c2e0dbd | 2013-05-09 18:35:53 +0100 | [diff] [blame] | 18 | #include "base/strings/sys_string_conversions.h" |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 19 | #include "chrome/app/chrome_command_ids.h" |
| 20 | #include "chrome/browser/autocomplete/autocomplete_classifier.h" |
| 21 | #include "chrome/browser/autocomplete/autocomplete_classifier_factory.h" |
| 22 | #include "chrome/browser/autocomplete/autocomplete_match.h" |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 23 | #include "chrome/browser/devtools/devtools_window.h" |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 24 | #include "chrome/browser/extensions/tab_helper.h" |
| 25 | #include "chrome/browser/favicon/favicon_tab_helper.h" |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 26 | #include "chrome/browser/profiles/profile.h" |
| 27 | #include "chrome/browser/profiles/profile_manager.h" |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 28 | #include "chrome/browser/themes/theme_service.h" |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 29 | #include "chrome/browser/ui/browser.h" |
| 30 | #include "chrome/browser/ui/browser_navigator.h" |
| 31 | #include "chrome/browser/ui/browser_tabstrip.h" |
| 32 | #import "chrome/browser/ui/cocoa/browser_window_controller.h" |
| 33 | #import "chrome/browser/ui/cocoa/constrained_window/constrained_window_sheet_controller.h" |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 34 | #include "chrome/browser/ui/cocoa/drag_util.h" |
| 35 | #import "chrome/browser/ui/cocoa/image_button_cell.h" |
| 36 | #import "chrome/browser/ui/cocoa/new_tab_button.h" |
| 37 | #import "chrome/browser/ui/cocoa/tab_contents/favicon_util_mac.h" |
| 38 | #import "chrome/browser/ui/cocoa/tab_contents/tab_contents_controller.h" |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 39 | #import "chrome/browser/ui/cocoa/tabs/tab_audio_indicator_view_mac.h" |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 40 | #import "chrome/browser/ui/cocoa/tabs/tab_controller.h" |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 41 | #import "chrome/browser/ui/cocoa/tabs/tab_projecting_image_view.h" |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 42 | #import "chrome/browser/ui/cocoa/tabs/tab_strip_drag_controller.h" |
| 43 | #import "chrome/browser/ui/cocoa/tabs/tab_strip_model_observer_bridge.h" |
| 44 | #import "chrome/browser/ui/cocoa/tabs/tab_strip_view.h" |
| 45 | #import "chrome/browser/ui/cocoa/tabs/tab_view.h" |
| 46 | #import "chrome/browser/ui/cocoa/tabs/throbber_view.h" |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 47 | #import "chrome/browser/ui/cocoa/tabs/throbbing_image_view.h" |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 48 | #include "chrome/browser/ui/find_bar/find_bar.h" |
| 49 | #include "chrome/browser/ui/find_bar/find_bar_controller.h" |
| 50 | #include "chrome/browser/ui/find_bar/find_tab_helper.h" |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 51 | #include "chrome/browser/ui/tabs/tab_menu_model.h" |
| 52 | #include "chrome/browser/ui/tabs/tab_strip_model.h" |
| 53 | #include "chrome/browser/ui/tabs/tab_strip_model_delegate.h" |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 54 | #include "chrome/browser/ui/tabs/tab_utils.h" |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 55 | #include "chrome/common/chrome_switches.h" |
Ben Murdoch | 558790d | 2013-07-30 15:19:42 +0100 | [diff] [blame] | 56 | #include "chrome/common/net/url_fixer_upper.h" |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 57 | #include "chrome/common/pref_names.h" |
Torne (Richard Coles) | 90dce4d | 2013-05-29 14:40:03 +0100 | [diff] [blame] | 58 | #include "components/web_modal/web_contents_modal_dialog_manager.h" |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 59 | #include "content/public/browser/navigation_controller.h" |
| 60 | #include "content/public/browser/user_metrics.h" |
| 61 | #include "content/public/browser/web_contents.h" |
| 62 | #include "content/public/browser/web_contents_view.h" |
| 63 | #include "grit/generated_resources.h" |
| 64 | #include "grit/theme_resources.h" |
| 65 | #include "grit/ui_resources.h" |
| 66 | #include "skia/ext/skia_utils_mac.h" |
| 67 | #import "third_party/GTM/AppKit/GTMNSAnimation+Duration.h" |
Torne (Richard Coles) | c2e0dbd | 2013-05-09 18:35:53 +0100 | [diff] [blame] | 68 | #import "ui/base/animation/animation_container.h" |
Torne (Richard Coles) | 7d4cd47 | 2013-06-19 11:58:07 +0100 | [diff] [blame] | 69 | #include "ui/base/cocoa/animation_utils.h" |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 70 | #import "ui/base/cocoa/tracking_area.h" |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 71 | #include "ui/base/l10n/l10n_util.h" |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 72 | #include "ui/base/models/list_selection_model.h" |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 73 | #include "ui/base/resource/resource_bundle.h" |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 74 | #include "ui/base/theme_provider.h" |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 75 | #include "ui/gfx/image/image.h" |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 76 | |
| 77 | using content::OpenURLParams; |
| 78 | using content::Referrer; |
| 79 | using content::UserMetricsAction; |
| 80 | using content::WebContents; |
| 81 | |
| 82 | namespace { |
| 83 | |
| 84 | // A value to indicate tab layout should use the full available width of the |
| 85 | // view. |
| 86 | const CGFloat kUseFullAvailableWidth = -1.0; |
| 87 | |
| 88 | // The amount by which tabs overlap. |
| 89 | // Needs to be <= the x position of the favicon within a tab. Else, every time |
| 90 | // the throbber is painted, the throbber's invalidation will also invalidate |
| 91 | // parts of the tab to the left, and two tabs's backgrounds need to be painted |
| 92 | // on each throbber frame instead of one. |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 93 | const CGFloat kTabOverlap = 19.0; |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 94 | |
| 95 | // The amount by which mini tabs are separated from normal tabs. |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 96 | const CGFloat kLastMiniTabSpacing = 2.0; |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 97 | |
| 98 | // The width and height for a tab's icon. |
| 99 | const CGFloat kIconWidthAndHeight = 16.0; |
| 100 | |
| 101 | // The amount by which the new tab button is offset (from the tabs). |
| 102 | const CGFloat kNewTabButtonOffset = 8.0; |
| 103 | |
| 104 | // Time (in seconds) in which tabs animate to their final position. |
| 105 | const NSTimeInterval kAnimationDuration = 0.125; |
| 106 | |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 107 | // The amount by which the profile menu button is offset (from tab tabs or new |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 108 | // tab button). |
| 109 | const CGFloat kProfileMenuButtonOffset = 6.0; |
| 110 | |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 111 | // The width and height of the icon + glow for projecting mode. |
| 112 | const CGFloat kProjectingIconWidthAndHeight = 32.0; |
| 113 | |
| 114 | // Throbbing duration on webrtc "this web page is watching you" favicon overlay. |
| 115 | const int kRecordingDurationMs = 1000; |
| 116 | |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 117 | // Helper class for doing NSAnimationContext calls that takes a bool to disable |
| 118 | // all the work. Useful for code that wants to conditionally animate. |
| 119 | class ScopedNSAnimationContextGroup { |
| 120 | public: |
| 121 | explicit ScopedNSAnimationContextGroup(bool animate) |
| 122 | : animate_(animate) { |
| 123 | if (animate_) { |
| 124 | [NSAnimationContext beginGrouping]; |
| 125 | } |
| 126 | } |
| 127 | |
| 128 | ~ScopedNSAnimationContextGroup() { |
| 129 | if (animate_) { |
| 130 | [NSAnimationContext endGrouping]; |
| 131 | } |
| 132 | } |
| 133 | |
| 134 | void SetCurrentContextDuration(NSTimeInterval duration) { |
| 135 | if (animate_) { |
| 136 | [[NSAnimationContext currentContext] gtm_setDuration:duration |
| 137 | eventMask:NSLeftMouseUpMask]; |
| 138 | } |
| 139 | } |
| 140 | |
| 141 | void SetCurrentContextShortestDuration() { |
| 142 | if (animate_) { |
| 143 | // The minimum representable time interval. This used to stop an |
| 144 | // in-progress animation as quickly as possible. |
| 145 | const NSTimeInterval kMinimumTimeInterval = |
| 146 | std::numeric_limits<NSTimeInterval>::min(); |
| 147 | // Directly set the duration to be short, avoiding the Steve slowmotion |
| 148 | // ettect the gtm_setDuration: provides. |
| 149 | [[NSAnimationContext currentContext] setDuration:kMinimumTimeInterval]; |
| 150 | } |
| 151 | } |
| 152 | |
| 153 | private: |
| 154 | bool animate_; |
| 155 | DISALLOW_COPY_AND_ASSIGN(ScopedNSAnimationContextGroup); |
| 156 | }; |
| 157 | |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 158 | // Creates an NSImage with size |size| and bitmap image representations for both |
| 159 | // 1x and 2x scale factors. |drawingHandler| is called once for every scale |
| 160 | // factor. This is similar to -[NSImage imageWithSize:flipped:drawingHandler:], |
| 161 | // but this function always evaluates drawingHandler eagerly, and it works on |
| 162 | // 10.6 and 10.7. |
| 163 | NSImage* CreateImageWithSize(NSSize size, |
| 164 | void (^drawingHandler)(NSSize)) { |
Ben Murdoch | eb525c5 | 2013-07-10 11:40:50 +0100 | [diff] [blame] | 165 | base::scoped_nsobject<NSImage> result([[NSImage alloc] initWithSize:size]); |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 166 | [NSGraphicsContext saveGraphicsState]; |
| 167 | for (ui::ScaleFactor scale_factor : ui::GetSupportedScaleFactors()) { |
| 168 | float scale = GetScaleFactorScale(scale_factor); |
| 169 | NSBitmapImageRep *bmpImageRep = [[NSBitmapImageRep alloc] |
| 170 | initWithBitmapDataPlanes:NULL |
| 171 | pixelsWide:size.width * scale |
| 172 | pixelsHigh:size.height * scale |
| 173 | bitsPerSample:8 |
| 174 | samplesPerPixel:4 |
| 175 | hasAlpha:YES |
| 176 | isPlanar:NO |
| 177 | colorSpaceName:NSDeviceRGBColorSpace |
| 178 | bytesPerRow:0 |
| 179 | bitsPerPixel:0]; |
| 180 | [bmpImageRep setSize:size]; |
| 181 | [NSGraphicsContext setCurrentContext: |
| 182 | [NSGraphicsContext graphicsContextWithBitmapImageRep:bmpImageRep]]; |
| 183 | drawingHandler(size); |
| 184 | [result addRepresentation:bmpImageRep]; |
| 185 | } |
| 186 | [NSGraphicsContext restoreGraphicsState]; |
| 187 | |
| 188 | return result.release(); |
| 189 | } |
| 190 | |
| 191 | // Takes a normal bitmap and a mask image and returns an image the size of the |
| 192 | // mask that has pixels from |image| but alpha information from |mask|. |
| 193 | NSImage* ApplyMask(NSImage* image, NSImage* mask) { |
| 194 | return [CreateImageWithSize([mask size], ^(NSSize size) { |
| 195 | // Skip a few pixels from the top of the tab background gradient, because |
| 196 | // the new tab button is not drawn at the very top of the browser window. |
| 197 | const int kYOffset = 10; |
| 198 | CGFloat width = size.width; |
| 199 | CGFloat height = size.height; |
| 200 | |
| 201 | // In some themes, the tab background image is narrower than the |
| 202 | // new tab button, so tile the background image. |
| 203 | CGFloat x = 0; |
| 204 | // The floor() is to make sure images with odd widths don't draw to the |
| 205 | // same pixel twice on retina displays. (Using NSDrawThreePartImage() |
| 206 | // caused a startup perf regression, so that cannot be used.) |
| 207 | CGFloat tileWidth = floor(std::min(width, [image size].width)); |
| 208 | while (x < width) { |
| 209 | [image drawAtPoint:NSMakePoint(x, 0) |
| 210 | fromRect:NSMakeRect(0, |
| 211 | [image size].height - height - kYOffset, |
| 212 | tileWidth, |
| 213 | height) |
| 214 | operation:NSCompositeCopy |
| 215 | fraction:1.0]; |
| 216 | x += tileWidth; |
| 217 | } |
| 218 | |
| 219 | [mask drawAtPoint:NSZeroPoint |
| 220 | fromRect:NSMakeRect(0, 0, width, height) |
| 221 | operation:NSCompositeDestinationIn |
| 222 | fraction:1.0]; |
| 223 | }) autorelease]; |
| 224 | } |
| 225 | |
| 226 | // Creates a modified favicon used for the recording case. The mask is used for |
| 227 | // making part of the favicon transparent. (The part where the recording dot |
| 228 | // later is drawn.) |
| 229 | NSImage* CreateMaskedFaviconForRecording(NSImage* image, |
| 230 | NSImage* mask, |
| 231 | NSImage* recImage) { |
| 232 | return [CreateImageWithSize([image size], ^(NSSize size) { |
| 233 | CGFloat width = size.width; |
| 234 | CGFloat height = size.height; |
| 235 | |
| 236 | [image drawAtPoint:NSZeroPoint |
| 237 | fromRect:NSMakeRect(0, 0, width, height) |
| 238 | operation:NSCompositeCopy |
| 239 | fraction:1.0]; |
| 240 | |
| 241 | NSSize maskSize = [mask size]; |
| 242 | NSSize recImageSize = [recImage size]; |
| 243 | CGFloat offsetFromRight = recImageSize.width + |
| 244 | (maskSize.width - recImageSize.width) / 2; |
| 245 | CGFloat offsetFromBottom = (maskSize.height - recImageSize.height) / 2; |
| 246 | |
| 247 | NSRect maskBounds; |
| 248 | maskBounds.origin.x = width - offsetFromRight; |
| 249 | maskBounds.origin.y = -offsetFromBottom; |
| 250 | maskBounds.size = maskSize; |
| 251 | |
| 252 | [mask drawInRect:maskBounds |
| 253 | fromRect:NSZeroRect |
| 254 | operation:NSCompositeDestinationOut |
| 255 | fraction:1.0]; |
| 256 | }) autorelease]; |
| 257 | } |
| 258 | |
| 259 | // Paints |overlay| on top of |ground|. |
| 260 | NSImage* Overlay(NSImage* ground, NSImage* overlay, CGFloat alpha) { |
| 261 | DCHECK_EQ([ground size].width, [overlay size].width); |
| 262 | DCHECK_EQ([ground size].height, [overlay size].height); |
| 263 | |
| 264 | return [CreateImageWithSize([ground size], ^(NSSize size) { |
| 265 | CGFloat width = size.width; |
| 266 | CGFloat height = size.height; |
| 267 | [ground drawAtPoint:NSZeroPoint |
| 268 | fromRect:NSMakeRect(0, 0, width, height) |
| 269 | operation:NSCompositeCopy |
| 270 | fraction:1.0]; |
| 271 | [overlay drawAtPoint:NSZeroPoint |
| 272 | fromRect:NSMakeRect(0, 0, width, height) |
| 273 | operation:NSCompositeSourceOver |
| 274 | fraction:alpha]; |
| 275 | }) autorelease]; |
| 276 | } |
| 277 | |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 278 | } // namespace |
| 279 | |
| 280 | @interface TabStripController (Private) |
| 281 | - (void)addSubviewToPermanentList:(NSView*)aView; |
| 282 | - (void)regenerateSubviewList; |
| 283 | - (NSInteger)indexForContentsView:(NSView*)view; |
| 284 | - (NSImageView*)iconImageViewForContents:(content::WebContents*)contents; |
| 285 | - (void)updateFaviconForContents:(content::WebContents*)contents |
| 286 | atIndex:(NSInteger)modelIndex; |
| 287 | - (void)layoutTabsWithAnimation:(BOOL)animate |
| 288 | regenerateSubviews:(BOOL)doUpdate; |
| 289 | - (void)animationDidStopForController:(TabController*)controller |
| 290 | finished:(BOOL)finished; |
| 291 | - (NSInteger)indexFromModelIndex:(NSInteger)index; |
| 292 | - (void)clickNewTabButton:(id)sender; |
| 293 | - (NSInteger)numberOfOpenTabs; |
| 294 | - (NSInteger)numberOfOpenMiniTabs; |
| 295 | - (NSInteger)numberOfOpenNonMiniTabs; |
| 296 | - (void)mouseMoved:(NSEvent*)event; |
| 297 | - (void)setTabTrackingAreasEnabled:(BOOL)enabled; |
| 298 | - (void)droppingURLsAt:(NSPoint)point |
| 299 | givesIndex:(NSInteger*)index |
| 300 | disposition:(WindowOpenDisposition*)disposition; |
| 301 | - (void)setNewTabButtonHoverState:(BOOL)showHover; |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 302 | - (void)themeDidChangeNotification:(NSNotification*)notification; |
| 303 | - (void)setNewTabImages; |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 304 | @end |
| 305 | |
| 306 | // A simple view class that prevents the Window Server from dragging the area |
| 307 | // behind tabs. Sometimes core animation confuses it. Unfortunately, it can also |
| 308 | // falsely pick up clicks during rapid tab closure, so we have to account for |
| 309 | // that. |
| 310 | @interface TabStripControllerDragBlockingView : NSView { |
| 311 | TabStripController* controller_; // weak; owns us |
| 312 | } |
| 313 | |
| 314 | - (id)initWithFrame:(NSRect)frameRect |
| 315 | controller:(TabStripController*)controller; |
| 316 | |
| 317 | // Runs a nested runloop to do window move tracking. Overriding |
| 318 | // -mouseDownCanMoveWindow with a dynamic result instead doesn't work: |
| 319 | // http://www.cocoabuilder.com/archive/cocoa/219261-conditional-mousedowncanmovewindow-for-nsview.html |
| 320 | // http://www.cocoabuilder.com/archive/cocoa/92973-brushed-metal-window-dragging.html |
| 321 | - (void)trackClickForWindowMove:(NSEvent*)event; |
| 322 | @end |
| 323 | |
| 324 | @implementation TabStripControllerDragBlockingView |
| 325 | - (BOOL)mouseDownCanMoveWindow { |
| 326 | return NO; |
| 327 | } |
| 328 | |
| 329 | - (void)drawRect:(NSRect)rect { |
| 330 | } |
| 331 | |
| 332 | - (id)initWithFrame:(NSRect)frameRect |
| 333 | controller:(TabStripController*)controller { |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 334 | if ((self = [super initWithFrame:frameRect])) { |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 335 | controller_ = controller; |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 336 | } |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 337 | return self; |
| 338 | } |
| 339 | |
| 340 | // In "rapid tab closure" mode (i.e., the user is clicking close tab buttons in |
| 341 | // rapid succession), the animations confuse Cocoa's hit testing (which appears |
| 342 | // to use cached results, among other tricks), so this view can somehow end up |
| 343 | // getting a mouse down event. Thus we do an explicit hit test during rapid tab |
| 344 | // closure, and if we find that we got a mouse down we shouldn't have, we send |
| 345 | // it off to the appropriate view. |
| 346 | - (void)mouseDown:(NSEvent*)event { |
| 347 | NSView* superview = [self superview]; |
| 348 | NSPoint hitLocation = |
| 349 | [[superview superview] convertPoint:[event locationInWindow] |
| 350 | fromView:nil]; |
| 351 | NSView* hitView = [superview hitTest:hitLocation]; |
| 352 | |
| 353 | if ([controller_ inRapidClosureMode]) { |
| 354 | if (hitView != self) { |
| 355 | [hitView mouseDown:event]; |
| 356 | return; |
| 357 | } |
| 358 | } |
| 359 | |
| 360 | if (hitView == self) { |
| 361 | BrowserWindowController* windowController = |
| 362 | [BrowserWindowController browserWindowControllerForView:self]; |
| 363 | if (![windowController isFullscreen]) { |
| 364 | [self trackClickForWindowMove:event]; |
| 365 | return; |
| 366 | } |
| 367 | } |
| 368 | [super mouseDown:event]; |
| 369 | } |
| 370 | |
| 371 | - (void)trackClickForWindowMove:(NSEvent*)event { |
| 372 | NSWindow* window = [self window]; |
| 373 | NSPoint frameOrigin = [window frame].origin; |
| 374 | NSPoint lastEventLoc = [window convertBaseToScreen:[event locationInWindow]]; |
| 375 | while ((event = [NSApp nextEventMatchingMask: |
| 376 | NSLeftMouseDownMask|NSLeftMouseDraggedMask|NSLeftMouseUpMask |
| 377 | untilDate:[NSDate distantFuture] |
| 378 | inMode:NSEventTrackingRunLoopMode |
| 379 | dequeue:YES]) && |
| 380 | [event type] != NSLeftMouseUp) { |
| 381 | base::mac::ScopedNSAutoreleasePool pool; |
| 382 | |
| 383 | NSPoint now = [window convertBaseToScreen:[event locationInWindow]]; |
| 384 | frameOrigin.x += now.x - lastEventLoc.x; |
| 385 | frameOrigin.y += now.y - lastEventLoc.y; |
| 386 | [window setFrameOrigin:frameOrigin]; |
| 387 | lastEventLoc = now; |
| 388 | } |
| 389 | } |
| 390 | |
| 391 | @end |
| 392 | |
| 393 | #pragma mark - |
| 394 | |
| 395 | // A delegate, owned by the CAAnimation system, that is alerted when the |
| 396 | // animation to close a tab is completed. Calls back to the given tab strip |
| 397 | // to let it know that |controller_| is ready to be removed from the model. |
| 398 | // Since we only maintain weak references, the tab strip must call -invalidate: |
| 399 | // to prevent the use of dangling pointers. |
| 400 | @interface TabCloseAnimationDelegate : NSObject { |
| 401 | @private |
| 402 | TabStripController* strip_; // weak; owns us indirectly |
| 403 | TabController* controller_; // weak |
| 404 | } |
| 405 | |
| 406 | // Will tell |strip| when the animation for |controller|'s view has completed. |
| 407 | // These should not be nil, and will not be retained. |
| 408 | - (id)initWithTabStrip:(TabStripController*)strip |
| 409 | tabController:(TabController*)controller; |
| 410 | |
| 411 | // Invalidates this object so that no further calls will be made to |
| 412 | // |strip_|. This should be called when |strip_| is released, to |
| 413 | // prevent attempts to call into the released object. |
| 414 | - (void)invalidate; |
| 415 | |
| 416 | // CAAnimation delegate method |
| 417 | - (void)animationDidStop:(CAAnimation*)animation finished:(BOOL)finished; |
| 418 | |
| 419 | @end |
| 420 | |
| 421 | @implementation TabCloseAnimationDelegate |
| 422 | |
| 423 | - (id)initWithTabStrip:(TabStripController*)strip |
| 424 | tabController:(TabController*)controller { |
| 425 | if ((self = [super init])) { |
| 426 | DCHECK(strip && controller); |
| 427 | strip_ = strip; |
| 428 | controller_ = controller; |
| 429 | } |
| 430 | return self; |
| 431 | } |
| 432 | |
| 433 | - (void)invalidate { |
| 434 | strip_ = nil; |
| 435 | controller_ = nil; |
| 436 | } |
| 437 | |
| 438 | - (void)animationDidStop:(CAAnimation*)animation finished:(BOOL)finished { |
| 439 | [strip_ animationDidStopForController:controller_ finished:finished]; |
| 440 | } |
| 441 | |
| 442 | @end |
| 443 | |
| 444 | #pragma mark - |
| 445 | |
| 446 | // In general, there is a one-to-one correspondence between TabControllers, |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 447 | // TabViews, TabContentsControllers, and the WebContents in the |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 448 | // TabStripModel. In the steady-state, the indices line up so an index coming |
| 449 | // from the model is directly mapped to the same index in the parallel arrays |
| 450 | // holding our views and controllers. This is also true when new tabs are |
| 451 | // created (even though there is a small period of animation) because the tab is |
| 452 | // present in the model while the TabView is animating into place. As a result, |
| 453 | // nothing special need be done to handle "new tab" animation. |
| 454 | // |
| 455 | // This all goes out the window with the "close tab" animation. The animation |
| 456 | // kicks off in |-tabDetachedWithContents:atIndex:| with the notification that |
| 457 | // the tab has been removed from the model. The simplest solution at this |
| 458 | // point would be to remove the views and controllers as well, however once |
| 459 | // the TabView is removed from the view list, the tab z-order code takes care of |
| 460 | // removing it from the tab strip and we'll get no animation. That means if |
| 461 | // there is to be any visible animation, the TabView needs to stay around until |
| 462 | // its animation is complete. In order to maintain consistency among the |
| 463 | // internal parallel arrays, this means all structures are kept around until |
| 464 | // the animation completes. At this point, though, the model and our internal |
| 465 | // structures are out of sync: the indices no longer line up. As a result, |
| 466 | // there is a concept of a "model index" which represents an index valid in |
| 467 | // the TabStripModel. During steady-state, the "model index" is just the same |
| 468 | // index as our parallel arrays (as above), but during tab close animations, |
| 469 | // it is different, offset by the number of tabs preceding the index which |
| 470 | // are undergoing tab closing animation. As a result, the caller needs to be |
| 471 | // careful to use the available conversion routines when accessing the internal |
| 472 | // parallel arrays (e.g., -indexFromModelIndex:). Care also needs to be taken |
| 473 | // during tab layout to ignore closing tabs in the total width calculations and |
| 474 | // in individual tab positioning (to avoid moving them right back to where they |
| 475 | // were). |
| 476 | // |
| 477 | // In order to prevent actions being taken on tabs which are closing, the tab |
| 478 | // itself gets marked as such so it no longer will send back its select action |
| 479 | // or allow itself to be dragged. In addition, drags on the tab strip as a |
| 480 | // whole are disabled while there are tabs closing. |
| 481 | |
| 482 | @implementation TabStripController |
| 483 | |
| 484 | @synthesize leftIndentForControls = leftIndentForControls_; |
| 485 | @synthesize rightIndentForControls = rightIndentForControls_; |
| 486 | |
| 487 | - (id)initWithView:(TabStripView*)view |
| 488 | switchView:(NSView*)switchView |
| 489 | browser:(Browser*)browser |
| 490 | delegate:(id<TabStripControllerDelegate>)delegate { |
| 491 | DCHECK(view && switchView && browser && delegate); |
| 492 | if ((self = [super init])) { |
| 493 | tabStripView_.reset([view retain]); |
| 494 | [tabStripView_ setController:self]; |
| 495 | switchView_ = switchView; |
| 496 | browser_ = browser; |
| 497 | tabStripModel_ = browser_->tab_strip_model(); |
| 498 | hoverTabSelector_.reset(new HoverTabSelector(tabStripModel_)); |
| 499 | delegate_ = delegate; |
| 500 | bridge_.reset(new TabStripModelObserverBridge(tabStripModel_, self)); |
| 501 | dragController_.reset( |
| 502 | [[TabStripDragController alloc] initWithTabStripController:self]); |
| 503 | tabContentsArray_.reset([[NSMutableArray alloc] init]); |
| 504 | tabArray_.reset([[NSMutableArray alloc] init]); |
Torne (Richard Coles) | c2e0dbd | 2013-05-09 18:35:53 +0100 | [diff] [blame] | 505 | animationContainer_ = new ui::AnimationContainer; |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 506 | NSWindow* browserWindow = [view window]; |
| 507 | |
| 508 | // Important note: any non-tab subviews not added to |permanentSubviews_| |
| 509 | // (see |-addSubviewToPermanentList:|) will be wiped out. |
| 510 | permanentSubviews_.reset([[NSMutableArray alloc] init]); |
| 511 | |
| 512 | ResourceBundle& rb = ResourceBundle::GetSharedInstance(); |
| 513 | defaultFavicon_.reset( |
| 514 | rb.GetNativeImageNamed(IDR_DEFAULT_FAVICON).CopyNSImage()); |
| 515 | |
| 516 | [self setLeftIndentForControls:[[self class] defaultLeftIndentForControls]]; |
| 517 | [self setRightIndentForControls:0]; |
| 518 | |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 519 | newTabButton_ = [view getNewTabButton]; |
| 520 | [self addSubviewToPermanentList:newTabButton_]; |
| 521 | [newTabButton_ setTarget:self]; |
| 522 | [newTabButton_ setAction:@selector(clickNewTabButton:)]; |
| 523 | |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 524 | [self setNewTabImages]; |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 525 | newTabButtonShowingHoverImage_ = NO; |
| 526 | newTabTrackingArea_.reset( |
| 527 | [[CrTrackingArea alloc] initWithRect:[newTabButton_ bounds] |
| 528 | options:(NSTrackingMouseEnteredAndExited | |
| 529 | NSTrackingActiveAlways) |
| 530 | owner:self |
| 531 | userInfo:nil]); |
| 532 | if (browserWindow) // Nil for Browsers without a tab strip (e.g. popups). |
| 533 | [newTabTrackingArea_ clearOwnerWhenWindowWillClose:browserWindow]; |
| 534 | [newTabButton_ addTrackingArea:newTabTrackingArea_.get()]; |
| 535 | targetFrames_.reset([[NSMutableDictionary alloc] init]); |
| 536 | |
| 537 | dragBlockingView_.reset( |
| 538 | [[TabStripControllerDragBlockingView alloc] initWithFrame:NSZeroRect |
| 539 | controller:self]); |
| 540 | [self addSubviewToPermanentList:dragBlockingView_]; |
| 541 | |
Ben Murdoch | 7dbb3d5 | 2013-07-17 14:55:54 +0100 | [diff] [blame] | 542 | newTabTargetFrame_ = NSZeroRect; |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 543 | availableResizeWidth_ = kUseFullAvailableWidth; |
| 544 | |
| 545 | closingControllers_.reset([[NSMutableSet alloc] init]); |
| 546 | |
| 547 | // Install the permanent subviews. |
| 548 | [self regenerateSubviewList]; |
| 549 | |
| 550 | // Watch for notifications that the tab strip view has changed size so |
| 551 | // we can tell it to layout for the new size. |
| 552 | [[NSNotificationCenter defaultCenter] |
| 553 | addObserver:self |
| 554 | selector:@selector(tabViewFrameChanged:) |
| 555 | name:NSViewFrameDidChangeNotification |
| 556 | object:tabStripView_]; |
| 557 | |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 558 | [[NSNotificationCenter defaultCenter] |
| 559 | addObserver:self |
| 560 | selector:@selector(themeDidChangeNotification:) |
| 561 | name:kBrowserThemeDidChangeNotification |
| 562 | object:nil]; |
| 563 | |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 564 | trackingArea_.reset([[CrTrackingArea alloc] |
| 565 | initWithRect:NSZeroRect // Ignored by NSTrackingInVisibleRect |
| 566 | options:NSTrackingMouseEnteredAndExited | |
| 567 | NSTrackingMouseMoved | |
| 568 | NSTrackingActiveAlways | |
| 569 | NSTrackingInVisibleRect |
| 570 | owner:self |
| 571 | userInfo:nil]); |
| 572 | if (browserWindow) // Nil for Browsers without a tab strip (e.g. popups). |
| 573 | [trackingArea_ clearOwnerWhenWindowWillClose:browserWindow]; |
| 574 | [tabStripView_ addTrackingArea:trackingArea_.get()]; |
| 575 | |
| 576 | // Check to see if the mouse is currently in our bounds so we can |
| 577 | // enable the tracking areas. Otherwise we won't get hover states |
| 578 | // or tab gradients if we load the window up under the mouse. |
| 579 | NSPoint mouseLoc = [[view window] mouseLocationOutsideOfEventStream]; |
| 580 | mouseLoc = [view convertPoint:mouseLoc fromView:nil]; |
| 581 | if (NSPointInRect(mouseLoc, [view bounds])) { |
| 582 | [self setTabTrackingAreasEnabled:YES]; |
| 583 | mouseInside_ = YES; |
| 584 | } |
| 585 | |
| 586 | // Set accessibility descriptions. http://openradar.appspot.com/7496255 |
| 587 | NSString* description = l10n_util::GetNSStringWithFixup(IDS_ACCNAME_NEWTAB); |
| 588 | [[newTabButton_ cell] |
| 589 | accessibilitySetOverrideValue:description |
| 590 | forAttribute:NSAccessibilityDescriptionAttribute]; |
| 591 | |
| 592 | // Controller may have been (re-)created by switching layout modes, which |
| 593 | // means the tab model is already fully formed with tabs. Need to walk the |
| 594 | // list and create the UI for each. |
| 595 | const int existingTabCount = tabStripModel_->count(); |
| 596 | const content::WebContents* selection = |
| 597 | tabStripModel_->GetActiveWebContents(); |
| 598 | for (int i = 0; i < existingTabCount; ++i) { |
| 599 | content::WebContents* currentContents = |
| 600 | tabStripModel_->GetWebContentsAt(i); |
| 601 | [self insertTabWithContents:currentContents |
| 602 | atIndex:i |
| 603 | inForeground:NO]; |
| 604 | if (selection == currentContents) { |
| 605 | // Must manually force a selection since the model won't send |
| 606 | // selection messages in this scenario. |
Torne (Richard Coles) | c2e0dbd | 2013-05-09 18:35:53 +0100 | [diff] [blame] | 607 | [self |
| 608 | activateTabWithContents:currentContents |
| 609 | previousContents:NULL |
| 610 | atIndex:i |
| 611 | reason:TabStripModelObserver::CHANGE_REASON_NONE]; |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 612 | } |
| 613 | } |
| 614 | // Don't lay out the tabs until after the controller has been fully |
| 615 | // constructed. |
| 616 | if (existingTabCount) { |
| 617 | [self performSelectorOnMainThread:@selector(layoutTabs) |
| 618 | withObject:nil |
| 619 | waitUntilDone:NO]; |
| 620 | } |
| 621 | } |
| 622 | return self; |
| 623 | } |
| 624 | |
| 625 | - (void)dealloc { |
| 626 | [tabStripView_ setController:nil]; |
| 627 | |
| 628 | if (trackingArea_.get()) |
| 629 | [tabStripView_ removeTrackingArea:trackingArea_.get()]; |
| 630 | |
| 631 | [newTabButton_ removeTrackingArea:newTabTrackingArea_.get()]; |
| 632 | // Invalidate all closing animations so they don't call back to us after |
| 633 | // we're gone. |
| 634 | for (TabController* controller in closingControllers_.get()) { |
| 635 | NSView* view = [controller view]; |
| 636 | [[[view animationForKey:@"frameOrigin"] delegate] invalidate]; |
| 637 | } |
| 638 | [[NSNotificationCenter defaultCenter] removeObserver:self]; |
| 639 | [tabStripView_ removeAllToolTips]; |
| 640 | [super dealloc]; |
| 641 | } |
| 642 | |
| 643 | + (CGFloat)defaultTabHeight { |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 644 | return 26.0; |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 645 | } |
| 646 | |
| 647 | + (CGFloat)defaultLeftIndentForControls { |
| 648 | // Default indentation leaves enough room so tabs don't overlap with the |
| 649 | // window controls. |
| 650 | return 70.0; |
| 651 | } |
| 652 | |
| 653 | // Finds the TabContentsController associated with the given index into the tab |
| 654 | // model and swaps out the sole child of the contentArea to display its |
| 655 | // contents. |
| 656 | - (void)swapInTabAtIndex:(NSInteger)modelIndex { |
| 657 | DCHECK(modelIndex >= 0 && modelIndex < tabStripModel_->count()); |
| 658 | NSInteger index = [self indexFromModelIndex:modelIndex]; |
| 659 | TabContentsController* controller = [tabContentsArray_ objectAtIndex:index]; |
| 660 | |
Torne (Richard Coles) | 7d4cd47 | 2013-06-19 11:58:07 +0100 | [diff] [blame] | 661 | // Make sure that any layers that move are not animated to their new |
| 662 | // positions. |
| 663 | ScopedCAActionDisabler disabler; |
| 664 | |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 665 | // Resize the new view to fit the window. Calling |view| may lazily |
| 666 | // instantiate the TabContentsController from the nib. Until we call |
| 667 | // |-ensureContentsVisible|, the controller doesn't install the RWHVMac into |
| 668 | // the view hierarchy. This is in order to avoid sending the renderer a |
| 669 | // spurious default size loaded from the nib during the call to |-view|. |
| 670 | NSView* newView = [controller view]; |
| 671 | |
| 672 | // Turns content autoresizing off, so removing and inserting views won't |
| 673 | // trigger unnecessary content relayout. |
| 674 | [controller ensureContentsSizeDoesNotChange]; |
| 675 | |
| 676 | // Remove the old view from the view hierarchy. We know there's only one |
| 677 | // child of |switchView_| because we're the one who put it there. There |
| 678 | // may not be any children in the case of a tab that's been closed, in |
| 679 | // which case there's no swapping going on. |
| 680 | NSArray* subviews = [switchView_ subviews]; |
| 681 | if ([subviews count]) { |
| 682 | NSView* oldView = [subviews objectAtIndex:0]; |
| 683 | // Set newView frame to the oldVew frame to prevent NSSplitView hosting |
| 684 | // sidebar and tab content from resizing sidebar's content view. |
| 685 | // ensureContentsVisible (see below) sets content size and autoresizing |
| 686 | // properties. |
| 687 | [newView setFrame:[oldView frame]]; |
| 688 | [switchView_ replaceSubview:oldView with:newView]; |
| 689 | } else { |
| 690 | [newView setFrame:[switchView_ bounds]]; |
| 691 | [switchView_ addSubview:newView]; |
| 692 | } |
| 693 | |
| 694 | // New content is in place, delegate should adjust itself accordingly. |
| 695 | [delegate_ onActivateTabWithContents:[controller webContents]]; |
| 696 | |
| 697 | // It also restores content autoresizing properties. |
| 698 | [controller ensureContentsVisible]; |
| 699 | |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 700 | NSWindow* parentWindow = [switchView_ window]; |
| 701 | ConstrainedWindowSheetController* sheetController = |
| 702 | [ConstrainedWindowSheetController |
| 703 | controllerForParentWindow:parentWindow]; |
| 704 | [sheetController parentViewDidBecomeActive:newView]; |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 705 | } |
| 706 | |
| 707 | // Create a new tab view and set its cell correctly so it draws the way we want |
| 708 | // it to. It will be sized and positioned by |-layoutTabs| so there's no need to |
| 709 | // set the frame here. This also creates the view as hidden, it will be |
| 710 | // shown during layout. |
| 711 | - (TabController*)newTab { |
| 712 | TabController* controller = [[[TabController alloc] init] autorelease]; |
| 713 | [controller setTarget:self]; |
| 714 | [controller setAction:@selector(selectTab:)]; |
| 715 | [[controller view] setHidden:YES]; |
| 716 | |
| 717 | return controller; |
| 718 | } |
| 719 | |
| 720 | // (Private) Handles a click on the new tab button. |
| 721 | - (void)clickNewTabButton:(id)sender { |
| 722 | content::RecordAction(UserMetricsAction("NewTab_Button")); |
| 723 | UMA_HISTOGRAM_ENUMERATION("Tab.NewTab", TabStripModel::NEW_TAB_BUTTON, |
| 724 | TabStripModel::NEW_TAB_ENUM_COUNT); |
| 725 | tabStripModel_->delegate()->AddBlankTabAt(-1, true); |
| 726 | } |
| 727 | |
| 728 | // (Private) Returns the number of open tabs in the tab strip. This is the |
| 729 | // number of TabControllers we know about (as there's a 1-to-1 mapping from |
| 730 | // these controllers to a tab) less the number of closing tabs. |
| 731 | - (NSInteger)numberOfOpenTabs { |
| 732 | return static_cast<NSInteger>(tabStripModel_->count()); |
| 733 | } |
| 734 | |
| 735 | // (Private) Returns the number of open, mini-tabs. |
| 736 | - (NSInteger)numberOfOpenMiniTabs { |
| 737 | // Ask the model for the number of mini tabs. Note that tabs which are in |
| 738 | // the process of closing (i.e., whose controllers are in |
| 739 | // |closingControllers_|) have already been removed from the model. |
| 740 | return tabStripModel_->IndexOfFirstNonMiniTab(); |
| 741 | } |
| 742 | |
| 743 | // (Private) Returns the number of open, non-mini tabs. |
| 744 | - (NSInteger)numberOfOpenNonMiniTabs { |
| 745 | NSInteger number = [self numberOfOpenTabs] - [self numberOfOpenMiniTabs]; |
| 746 | DCHECK_GE(number, 0); |
| 747 | return number; |
| 748 | } |
| 749 | |
| 750 | // Given an index into the tab model, returns the index into the tab controller |
| 751 | // or tab contents controller array accounting for tabs that are currently |
| 752 | // closing. For example, if there are two tabs in the process of closing before |
| 753 | // |index|, this returns |index| + 2. If there are no closing tabs, this will |
| 754 | // return |index|. |
| 755 | - (NSInteger)indexFromModelIndex:(NSInteger)index { |
| 756 | DCHECK_GE(index, 0); |
| 757 | if (index < 0) |
| 758 | return index; |
| 759 | |
| 760 | NSInteger i = 0; |
| 761 | for (TabController* controller in tabArray_.get()) { |
| 762 | if ([closingControllers_ containsObject:controller]) { |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 763 | DCHECK([[controller tabView] isClosing]); |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 764 | ++index; |
| 765 | } |
| 766 | if (i == index) // No need to check anything after, it has no effect. |
| 767 | break; |
| 768 | ++i; |
| 769 | } |
| 770 | return index; |
| 771 | } |
| 772 | |
| 773 | // Given an index into |tabArray_|, return the corresponding index into |
| 774 | // |tabStripModel_| or NSNotFound if the specified tab does not exist in |
| 775 | // the model (if it's closing, for example). |
| 776 | - (NSInteger)modelIndexFromIndex:(NSInteger)index { |
| 777 | NSInteger modelIndex = 0; |
| 778 | NSInteger arrayIndex = 0; |
| 779 | for (TabController* controller in tabArray_.get()) { |
| 780 | if (![closingControllers_ containsObject:controller]) { |
| 781 | if (arrayIndex == index) |
| 782 | return modelIndex; |
| 783 | ++modelIndex; |
| 784 | } else if (arrayIndex == index) { |
| 785 | // Tab is closing - no model index. |
| 786 | return NSNotFound; |
| 787 | } |
| 788 | ++arrayIndex; |
| 789 | } |
| 790 | return NSNotFound; |
| 791 | } |
| 792 | |
| 793 | // Returns the index of the subview |view|. Returns -1 if not present. Takes |
| 794 | // closing tabs into account such that this index will correctly match the tab |
| 795 | // model. If |view| is in the process of closing, returns -1, as closing tabs |
| 796 | // are no longer in the model. |
| 797 | - (NSInteger)modelIndexForTabView:(NSView*)view { |
| 798 | NSInteger index = 0; |
| 799 | for (TabController* current in tabArray_.get()) { |
| 800 | // If |current| is closing, skip it. |
| 801 | if ([closingControllers_ containsObject:current]) |
| 802 | continue; |
| 803 | else if ([current view] == view) |
| 804 | return index; |
| 805 | ++index; |
| 806 | } |
| 807 | return -1; |
| 808 | } |
| 809 | |
| 810 | // Returns the index of the contents subview |view|. Returns -1 if not present. |
| 811 | // Takes closing tabs into account such that this index will correctly match the |
| 812 | // tab model. If |view| is in the process of closing, returns -1, as closing |
| 813 | // tabs are no longer in the model. |
| 814 | - (NSInteger)modelIndexForContentsView:(NSView*)view { |
| 815 | NSInteger index = 0; |
| 816 | NSInteger i = 0; |
| 817 | for (TabContentsController* current in tabContentsArray_.get()) { |
| 818 | // If the TabController corresponding to |current| is closing, skip it. |
| 819 | TabController* controller = [tabArray_ objectAtIndex:i]; |
| 820 | if ([closingControllers_ containsObject:controller]) { |
| 821 | ++i; |
| 822 | continue; |
| 823 | } else if ([current view] == view) { |
| 824 | return index; |
| 825 | } |
| 826 | ++index; |
| 827 | ++i; |
| 828 | } |
| 829 | return -1; |
| 830 | } |
| 831 | |
| 832 | |
| 833 | // Returns the view at the given index, using the array of TabControllers to |
| 834 | // get the associated view. Returns nil if out of range. |
| 835 | - (NSView*)viewAtIndex:(NSUInteger)index { |
| 836 | if (index >= [tabArray_ count]) |
| 837 | return NULL; |
| 838 | return [[tabArray_ objectAtIndex:index] view]; |
| 839 | } |
| 840 | |
| 841 | - (NSUInteger)viewsCount { |
| 842 | return [tabArray_ count]; |
| 843 | } |
| 844 | |
| 845 | // Called when the user clicks a tab. Tell the model the selection has changed, |
| 846 | // which feeds back into us via a notification. |
| 847 | - (void)selectTab:(id)sender { |
| 848 | DCHECK([sender isKindOfClass:[NSView class]]); |
| 849 | int index = [self modelIndexForTabView:sender]; |
| 850 | NSUInteger modifiers = [[NSApp currentEvent] modifierFlags]; |
| 851 | if (tabStripModel_->ContainsIndex(index)) { |
| 852 | if (modifiers & NSCommandKeyMask && modifiers & NSShiftKeyMask) { |
| 853 | tabStripModel_->AddSelectionFromAnchorTo(index); |
| 854 | } else if (modifiers & NSShiftKeyMask) { |
| 855 | tabStripModel_->ExtendSelectionTo(index); |
| 856 | } else if (modifiers & NSCommandKeyMask) { |
| 857 | tabStripModel_->ToggleSelectionAt(index); |
| 858 | } else if (!tabStripModel_->IsTabSelected(index)) { |
| 859 | tabStripModel_->ActivateTabAt(index, true); |
| 860 | } |
| 861 | } |
| 862 | } |
| 863 | |
| 864 | // Called when the user closes a tab. Asks the model to close the tab. |sender| |
| 865 | // is the TabView that is potentially going away. |
| 866 | - (void)closeTab:(id)sender { |
| 867 | DCHECK([sender isKindOfClass:[TabView class]]); |
| 868 | |
| 869 | // Cancel any pending tab transition. |
| 870 | hoverTabSelector_->CancelTabTransition(); |
| 871 | |
| 872 | if ([hoveredTab_ isEqual:sender]) { |
| 873 | hoveredTab_ = nil; |
| 874 | } |
| 875 | |
| 876 | NSInteger index = [self modelIndexForTabView:sender]; |
| 877 | if (!tabStripModel_->ContainsIndex(index)) |
| 878 | return; |
| 879 | |
| 880 | content::RecordAction(UserMetricsAction("CloseTab_Mouse")); |
| 881 | const NSInteger numberOfOpenTabs = [self numberOfOpenTabs]; |
| 882 | if (numberOfOpenTabs > 1) { |
| 883 | bool isClosingLastTab = index == numberOfOpenTabs - 1; |
| 884 | if (!isClosingLastTab) { |
| 885 | // Limit the width available for laying out tabs so that tabs are not |
| 886 | // resized until a later time (when the mouse leaves the tab strip). |
| 887 | // However, if the tab being closed is a pinned tab, break out of |
| 888 | // rapid-closure mode since the mouse is almost guaranteed not to be over |
| 889 | // the closebox of the adjacent tab (due to the difference in widths). |
| 890 | // TODO(pinkerton): re-visit when handling tab overflow. |
| 891 | // http://crbug.com/188 |
| 892 | if (tabStripModel_->IsTabPinned(index)) { |
| 893 | availableResizeWidth_ = kUseFullAvailableWidth; |
| 894 | } else { |
| 895 | NSView* penultimateTab = [self viewAtIndex:numberOfOpenTabs - 2]; |
| 896 | availableResizeWidth_ = NSMaxX([penultimateTab frame]); |
| 897 | } |
| 898 | } else { |
| 899 | // If the rightmost tab is closed, change the available width so that |
| 900 | // another tab's close button lands below the cursor (assuming the tabs |
| 901 | // are currently below their maximum width and can grow). |
| 902 | NSView* lastTab = [self viewAtIndex:numberOfOpenTabs - 1]; |
| 903 | availableResizeWidth_ = NSMaxX([lastTab frame]); |
| 904 | } |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 905 | tabStripModel_->CloseWebContentsAt( |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 906 | index, |
| 907 | TabStripModel::CLOSE_USER_GESTURE | |
| 908 | TabStripModel::CLOSE_CREATE_HISTORICAL_TAB); |
| 909 | } else { |
| 910 | // Use the standard window close if this is the last tab |
| 911 | // this prevents the tab from being removed from the model until after |
| 912 | // the window dissapears |
| 913 | [[tabStripView_ window] performClose:nil]; |
| 914 | } |
| 915 | } |
| 916 | |
| 917 | // Dispatch context menu commands for the given tab controller. |
| 918 | - (void)commandDispatch:(TabStripModel::ContextMenuCommand)command |
| 919 | forController:(TabController*)controller { |
| 920 | int index = [self modelIndexForTabView:[controller view]]; |
| 921 | if (tabStripModel_->ContainsIndex(index)) |
| 922 | tabStripModel_->ExecuteContextMenuCommand(index, command); |
| 923 | } |
| 924 | |
| 925 | // Returns YES if the specificed command should be enabled for the given |
| 926 | // controller. |
| 927 | - (BOOL)isCommandEnabled:(TabStripModel::ContextMenuCommand)command |
| 928 | forController:(TabController*)controller { |
| 929 | int index = [self modelIndexForTabView:[controller view]]; |
| 930 | if (!tabStripModel_->ContainsIndex(index)) |
| 931 | return NO; |
| 932 | return tabStripModel_->IsContextMenuCommandEnabled(index, command) ? YES : NO; |
| 933 | } |
| 934 | |
| 935 | // Returns a context menu model for a given controller. Caller owns the result. |
| 936 | - (ui::SimpleMenuModel*)contextMenuModelForController:(TabController*)controller |
| 937 | menuDelegate:(ui::SimpleMenuModel::Delegate*)delegate { |
| 938 | int index = [self modelIndexForTabView:[controller view]]; |
| 939 | return new TabMenuModel(delegate, tabStripModel_, index); |
| 940 | } |
| 941 | |
| 942 | // Returns a weak reference to the controller that manages dragging of tabs. |
| 943 | - (id<TabDraggingEventTarget>)dragController { |
| 944 | return dragController_.get(); |
| 945 | } |
| 946 | |
| 947 | - (void)insertPlaceholderForTab:(TabView*)tab frame:(NSRect)frame { |
| 948 | placeholderTab_ = tab; |
| 949 | placeholderFrame_ = frame; |
| 950 | [self layoutTabsWithAnimation:initialLayoutComplete_ regenerateSubviews:NO]; |
| 951 | } |
| 952 | |
| 953 | - (BOOL)isDragSessionActive { |
| 954 | return placeholderTab_ != nil; |
| 955 | } |
| 956 | |
| 957 | - (BOOL)isTabFullyVisible:(TabView*)tab { |
| 958 | NSRect frame = [tab frame]; |
| 959 | return NSMinX(frame) >= [self leftIndentForControls] && |
| 960 | NSMaxX(frame) <= (NSMaxX([tabStripView_ frame]) - |
| 961 | [self rightIndentForControls]); |
| 962 | } |
| 963 | |
| 964 | - (void)showNewTabButton:(BOOL)show { |
| 965 | forceNewTabButtonHidden_ = show ? NO : YES; |
| 966 | if (forceNewTabButtonHidden_) |
| 967 | [newTabButton_ setHidden:YES]; |
| 968 | } |
| 969 | |
| 970 | // Lay out all tabs in the order of their TabContentsControllers, which matches |
| 971 | // the ordering in the TabStripModel. This call isn't that expensive, though |
| 972 | // it is O(n) in the number of tabs. Tabs will animate to their new position |
| 973 | // if the window is visible and |animate| is YES. |
| 974 | // TODO(pinkerton): Note this doesn't do too well when the number of min-sized |
| 975 | // tabs would cause an overflow. http://crbug.com/188 |
| 976 | - (void)layoutTabsWithAnimation:(BOOL)animate |
| 977 | regenerateSubviews:(BOOL)doUpdate { |
| 978 | DCHECK([NSThread isMainThread]); |
| 979 | if (![tabArray_ count]) |
| 980 | return; |
| 981 | |
| 982 | const CGFloat kMaxTabWidth = [TabController maxTabWidth]; |
| 983 | const CGFloat kMinTabWidth = [TabController minTabWidth]; |
| 984 | const CGFloat kMinSelectedTabWidth = [TabController minSelectedTabWidth]; |
| 985 | const CGFloat kMiniTabWidth = [TabController miniTabWidth]; |
| 986 | const CGFloat kAppTabWidth = [TabController appTabWidth]; |
| 987 | |
| 988 | NSRect enclosingRect = NSZeroRect; |
| 989 | ScopedNSAnimationContextGroup mainAnimationGroup(animate); |
| 990 | mainAnimationGroup.SetCurrentContextDuration(kAnimationDuration); |
| 991 | |
| 992 | // Update the current subviews and their z-order if requested. |
| 993 | if (doUpdate) |
| 994 | [self regenerateSubviewList]; |
| 995 | |
| 996 | // Compute the base width of tabs given how much room we're allowed. Note that |
| 997 | // mini-tabs have a fixed width. We may not be able to use the entire width |
| 998 | // if the user is quickly closing tabs. This may be negative, but that's okay |
| 999 | // (taken care of by |MAX()| when calculating tab sizes). |
| 1000 | CGFloat availableSpace = 0; |
| 1001 | if ([self inRapidClosureMode]) { |
| 1002 | availableSpace = availableResizeWidth_; |
| 1003 | } else { |
| 1004 | availableSpace = NSWidth([tabStripView_ frame]); |
| 1005 | |
| 1006 | // Account for the width of the new tab button. |
| 1007 | availableSpace -= NSWidth([newTabButton_ frame]) + kNewTabButtonOffset; |
| 1008 | |
| 1009 | // Account for the right-side controls if not in rapid closure mode. |
| 1010 | // (In rapid closure mode, the available width is set based on the |
| 1011 | // position of the rightmost tab, not based on the width of the tab strip, |
| 1012 | // so the right controls have already been accounted for.) |
| 1013 | availableSpace -= [self rightIndentForControls]; |
| 1014 | } |
| 1015 | |
| 1016 | // Need to leave room for the left-side controls even in rapid closure mode. |
| 1017 | availableSpace -= [self leftIndentForControls]; |
| 1018 | |
| 1019 | // If there are any mini tabs, account for the extra spacing between the last |
| 1020 | // mini tab and the first regular tab. |
| 1021 | if ([self numberOfOpenMiniTabs]) |
| 1022 | availableSpace -= kLastMiniTabSpacing; |
| 1023 | |
| 1024 | // This may be negative, but that's okay (taken care of by |MAX()| when |
| 1025 | // calculating tab sizes). "mini" tabs in horizontal mode just get a special |
| 1026 | // section, they don't change size. |
| 1027 | CGFloat availableSpaceForNonMini = availableSpace; |
| 1028 | availableSpaceForNonMini -= |
| 1029 | [self numberOfOpenMiniTabs] * (kMiniTabWidth - kTabOverlap); |
| 1030 | |
| 1031 | // Initialize |nonMiniTabWidth| in case there aren't any non-mini-tabs; this |
| 1032 | // value shouldn't actually be used. |
| 1033 | CGFloat nonMiniTabWidth = kMaxTabWidth; |
| 1034 | CGFloat nonMiniTabWidthFraction = 0; |
| 1035 | const NSInteger numberOfOpenNonMiniTabs = [self numberOfOpenNonMiniTabs]; |
| 1036 | if (numberOfOpenNonMiniTabs) { |
| 1037 | // Find the width of a non-mini-tab. This only applies to horizontal |
| 1038 | // mode. Add in the amount we "get back" from the tabs overlapping. |
| 1039 | availableSpaceForNonMini += (numberOfOpenNonMiniTabs - 1) * kTabOverlap; |
| 1040 | |
| 1041 | // Divide up the space between the non-mini-tabs. |
| 1042 | nonMiniTabWidth = availableSpaceForNonMini / numberOfOpenNonMiniTabs; |
| 1043 | |
| 1044 | // Clamp the width between the max and min. |
| 1045 | nonMiniTabWidth = MAX(MIN(nonMiniTabWidth, kMaxTabWidth), kMinTabWidth); |
| 1046 | |
| 1047 | // Separate integral and fractional parts. |
| 1048 | CGFloat integralPart = std::floor(nonMiniTabWidth); |
| 1049 | nonMiniTabWidthFraction = nonMiniTabWidth - integralPart; |
| 1050 | nonMiniTabWidth = integralPart; |
| 1051 | } |
| 1052 | |
| 1053 | BOOL visible = [[tabStripView_ window] isVisible]; |
| 1054 | |
| 1055 | CGFloat offset = [self leftIndentForControls]; |
| 1056 | bool hasPlaceholderGap = false; |
| 1057 | // Whether or not the last tab processed by the loop was a mini tab. |
| 1058 | BOOL isLastTabMini = NO; |
| 1059 | CGFloat tabWidthAccumulatedFraction = 0; |
| 1060 | NSInteger laidOutNonMiniTabs = 0; |
| 1061 | |
| 1062 | // Remove all the tooltip rects on the tab strip so that we can re-apply |
| 1063 | // them to correspond with the new tab positions. |
| 1064 | [tabStripView_ removeAllToolTips]; |
| 1065 | |
| 1066 | for (TabController* tab in tabArray_.get()) { |
| 1067 | // Ignore a tab that is going through a close animation. |
| 1068 | if ([closingControllers_ containsObject:tab]) |
| 1069 | continue; |
| 1070 | |
| 1071 | BOOL isPlaceholder = [[tab view] isEqual:placeholderTab_]; |
| 1072 | NSRect tabFrame = [[tab view] frame]; |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 1073 | tabFrame.size.height = [[self class] defaultTabHeight]; |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 1074 | tabFrame.origin.y = 0; |
| 1075 | tabFrame.origin.x = offset; |
| 1076 | |
| 1077 | // If the tab is hidden, we consider it a new tab. We make it visible |
| 1078 | // and animate it in. |
| 1079 | BOOL newTab = [[tab view] isHidden]; |
| 1080 | if (newTab) |
| 1081 | [[tab view] setHidden:NO]; |
| 1082 | |
| 1083 | if (isPlaceholder) { |
| 1084 | // Move the current tab to the correct location instantly. |
| 1085 | // We need a duration or else it doesn't cancel an inflight animation. |
| 1086 | ScopedNSAnimationContextGroup localAnimationGroup(animate); |
| 1087 | localAnimationGroup.SetCurrentContextShortestDuration(); |
| 1088 | tabFrame.origin.x = placeholderFrame_.origin.x; |
| 1089 | id target = animate ? [[tab view] animator] : [tab view]; |
| 1090 | [target setFrame:tabFrame]; |
| 1091 | |
| 1092 | // Store the frame by identifier to avoid redundant calls to animator. |
| 1093 | NSValue* identifier = [NSValue valueWithPointer:[tab view]]; |
| 1094 | [targetFrames_ setObject:[NSValue valueWithRect:tabFrame] |
| 1095 | forKey:identifier]; |
| 1096 | continue; |
| 1097 | } |
| 1098 | |
| 1099 | if (placeholderTab_ && !hasPlaceholderGap) { |
| 1100 | const CGFloat placeholderMin = NSMinX(placeholderFrame_); |
| 1101 | // If the left edge is to the left of the placeholder's left, but the |
| 1102 | // mid is to the right of it slide over to make space for it. |
| 1103 | if (NSMidX(tabFrame) > placeholderMin) { |
| 1104 | hasPlaceholderGap = true; |
| 1105 | offset += NSWidth(placeholderFrame_); |
| 1106 | offset -= kTabOverlap; |
| 1107 | tabFrame.origin.x = offset; |
| 1108 | } |
| 1109 | } |
| 1110 | |
| 1111 | // Set the width. Selected tabs are slightly wider when things get really |
| 1112 | // small and thus we enforce a different minimum width. |
| 1113 | BOOL isMini = [tab mini]; |
| 1114 | if (isMini) { |
| 1115 | tabFrame.size.width = [tab app] ? kAppTabWidth : kMiniTabWidth; |
| 1116 | } else { |
| 1117 | // Tabs have non-integer widths. Assign the integer part to the tab, and |
| 1118 | // keep an accumulation of the fractional parts. When the fractional |
| 1119 | // accumulation gets to be more than one pixel, assign that to the current |
| 1120 | // tab being laid out. This is vaguely inspired by Bresenham's line |
| 1121 | // algorithm. |
| 1122 | tabFrame.size.width = nonMiniTabWidth; |
| 1123 | tabWidthAccumulatedFraction += nonMiniTabWidthFraction; |
| 1124 | |
| 1125 | if (tabWidthAccumulatedFraction >= 1.0) { |
| 1126 | ++tabFrame.size.width; |
| 1127 | --tabWidthAccumulatedFraction; |
| 1128 | } |
| 1129 | |
| 1130 | // In case of rounding error, give any left over pixels to the last tab. |
| 1131 | if (laidOutNonMiniTabs == numberOfOpenNonMiniTabs - 1 && |
| 1132 | tabWidthAccumulatedFraction > 0.5) { |
| 1133 | ++tabFrame.size.width; |
| 1134 | } |
| 1135 | |
| 1136 | ++laidOutNonMiniTabs; |
| 1137 | } |
| 1138 | |
| 1139 | if ([tab selected]) |
| 1140 | tabFrame.size.width = MAX(tabFrame.size.width, kMinSelectedTabWidth); |
| 1141 | |
| 1142 | // If this is the first non-mini tab, then add a bit of spacing between this |
| 1143 | // and the last mini tab. |
| 1144 | if (!isMini && isLastTabMini) { |
| 1145 | offset += kLastMiniTabSpacing; |
| 1146 | tabFrame.origin.x = offset; |
| 1147 | } |
| 1148 | isLastTabMini = isMini; |
| 1149 | |
| 1150 | // Animate a new tab in by putting it below the horizon unless told to put |
| 1151 | // it in a specific location (i.e., from a drop). |
| 1152 | if (newTab && visible && animate) { |
| 1153 | if (NSEqualRects(droppedTabFrame_, NSZeroRect)) { |
| 1154 | [[tab view] setFrame:NSOffsetRect(tabFrame, 0, -NSHeight(tabFrame))]; |
| 1155 | } else { |
| 1156 | [[tab view] setFrame:droppedTabFrame_]; |
| 1157 | droppedTabFrame_ = NSZeroRect; |
| 1158 | } |
| 1159 | } |
| 1160 | |
| 1161 | // Check the frame by identifier to avoid redundant calls to animator. |
| 1162 | id frameTarget = visible && animate ? [[tab view] animator] : [tab view]; |
| 1163 | NSValue* identifier = [NSValue valueWithPointer:[tab view]]; |
| 1164 | NSValue* oldTargetValue = [targetFrames_ objectForKey:identifier]; |
| 1165 | if (!oldTargetValue || |
| 1166 | !NSEqualRects([oldTargetValue rectValue], tabFrame)) { |
| 1167 | [frameTarget setFrame:tabFrame]; |
| 1168 | [targetFrames_ setObject:[NSValue valueWithRect:tabFrame] |
| 1169 | forKey:identifier]; |
| 1170 | } |
| 1171 | |
| 1172 | enclosingRect = NSUnionRect(tabFrame, enclosingRect); |
| 1173 | |
| 1174 | offset += NSWidth(tabFrame); |
| 1175 | offset -= kTabOverlap; |
| 1176 | |
| 1177 | // Create a rect which starts at the point where the tab overlap will end so |
| 1178 | // that as the mouse cursor crosses over the boundary it will get updated. |
| 1179 | // The inset is based on a multiplier of the height. |
| 1180 | float insetWidth = NSHeight(tabFrame) * [TabView insetMultiplier]; |
| 1181 | // NSInsetRect will also expose the "insetWidth" at the right of the tab. |
| 1182 | NSRect tabToolTipRect = NSInsetRect(tabFrame, insetWidth, 0); |
| 1183 | [tabStripView_ addToolTipRect:tabToolTipRect owner:self userData:nil]; |
| 1184 | |
| 1185 | // Also create two more rects in the remaining space so that the tooltip |
| 1186 | // is more likely to get updated crossing tabs. |
| 1187 | // These rects "cover" the right edge of the previous tab that was exposed |
| 1188 | // since the tabs overlap. |
| 1189 | tabToolTipRect = tabFrame; |
| 1190 | tabToolTipRect.size.width = insetWidth / 2.0; |
| 1191 | [tabStripView_ addToolTipRect:tabToolTipRect owner:self userData:nil]; |
| 1192 | |
| 1193 | tabToolTipRect = NSOffsetRect(tabToolTipRect, insetWidth / 2.0, 0); |
| 1194 | [tabStripView_ addToolTipRect:tabToolTipRect owner:self userData:nil]; |
| 1195 | } |
| 1196 | |
| 1197 | // Hide the new tab button if we're explicitly told to. It may already |
| 1198 | // be hidden, doing it again doesn't hurt. Otherwise position it |
| 1199 | // appropriately, showing it if necessary. |
| 1200 | if (forceNewTabButtonHidden_) { |
| 1201 | [newTabButton_ setHidden:YES]; |
| 1202 | } else { |
| 1203 | NSRect newTabNewFrame = [newTabButton_ frame]; |
| 1204 | // We've already ensured there's enough space for the new tab button |
| 1205 | // so we don't have to check it against the available space. We do need |
| 1206 | // to make sure we put it after any placeholder. |
| 1207 | CGFloat maxTabX = MAX(offset, NSMaxX(placeholderFrame_) - kTabOverlap); |
| 1208 | newTabNewFrame.origin = NSMakePoint(maxTabX + kNewTabButtonOffset, 0); |
| 1209 | if ([tabContentsArray_ count]) |
| 1210 | [newTabButton_ setHidden:NO]; |
| 1211 | |
| 1212 | if (!NSEqualRects(newTabTargetFrame_, newTabNewFrame)) { |
| 1213 | // Set the new tab button image correctly based on where the cursor is. |
| 1214 | NSWindow* window = [tabStripView_ window]; |
| 1215 | NSPoint currentMouse = [window mouseLocationOutsideOfEventStream]; |
| 1216 | currentMouse = [tabStripView_ convertPoint:currentMouse fromView:nil]; |
| 1217 | |
| 1218 | BOOL shouldShowHover = [newTabButton_ pointIsOverButton:currentMouse]; |
| 1219 | [self setNewTabButtonHoverState:shouldShowHover]; |
| 1220 | |
| 1221 | // Move the new tab button into place. We want to animate the new tab |
| 1222 | // button if it's moving to the left (closing a tab), but not when it's |
| 1223 | // moving to the right (inserting a new tab). If moving right, we need |
| 1224 | // to use a very small duration to make sure we cancel any in-flight |
| 1225 | // animation to the left. |
| 1226 | if (visible && animate) { |
| 1227 | ScopedNSAnimationContextGroup localAnimationGroup(true); |
| 1228 | BOOL movingLeft = NSMinX(newTabNewFrame) < NSMinX(newTabTargetFrame_); |
| 1229 | if (!movingLeft) { |
| 1230 | localAnimationGroup.SetCurrentContextShortestDuration(); |
| 1231 | } |
| 1232 | [[newTabButton_ animator] setFrame:newTabNewFrame]; |
| 1233 | newTabTargetFrame_ = newTabNewFrame; |
| 1234 | } else { |
| 1235 | [newTabButton_ setFrame:newTabNewFrame]; |
| 1236 | newTabTargetFrame_ = newTabNewFrame; |
| 1237 | } |
| 1238 | } |
| 1239 | } |
| 1240 | |
| 1241 | [dragBlockingView_ setFrame:enclosingRect]; |
| 1242 | |
| 1243 | // Add a catch-all tooltip rect which will handle any remaining tab strip |
| 1244 | // region not covered by tab-specific rects. |
| 1245 | [tabStripView_ addToolTipRect:enclosingRect owner:self userData:nil]; |
| 1246 | |
| 1247 | // Mark that we've successfully completed layout of at least one tab. |
| 1248 | initialLayoutComplete_ = YES; |
| 1249 | } |
| 1250 | |
| 1251 | // Return the current hovered tab's tooltip when requested by the tooltip |
| 1252 | // manager. |
| 1253 | - (NSString*) view:(NSView*)view |
| 1254 | stringForToolTip:(NSToolTipTag)tag |
| 1255 | point:(NSPoint)point |
| 1256 | userData:(void*)data { |
| 1257 | return [hoveredTab_ toolTipText]; |
| 1258 | } |
| 1259 | |
| 1260 | // When we're told to layout from the public API we usually want to animate, |
| 1261 | // except when it's the first time. |
| 1262 | - (void)layoutTabs { |
| 1263 | [self layoutTabsWithAnimation:initialLayoutComplete_ regenerateSubviews:YES]; |
| 1264 | } |
| 1265 | |
| 1266 | - (void)layoutTabsWithoutAnimation { |
| 1267 | [self layoutTabsWithAnimation:NO regenerateSubviews:YES]; |
| 1268 | } |
| 1269 | |
| 1270 | // Handles setting the title of the tab based on the given |contents|. Uses |
| 1271 | // a canned string if |contents| is NULL. |
| 1272 | - (void)setTabTitle:(NSViewController*)tab withContents:(WebContents*)contents { |
| 1273 | NSString* titleString = nil; |
| 1274 | if (contents) |
| 1275 | titleString = base::SysUTF16ToNSString(contents->GetTitle()); |
| 1276 | if (![titleString length]) { |
| 1277 | titleString = l10n_util::GetNSString(IDS_BROWSER_WINDOW_MAC_TAB_UNTITLED); |
| 1278 | } |
| 1279 | [tab setTitle:titleString]; |
| 1280 | } |
| 1281 | |
| 1282 | // Called when a notification is received from the model to insert a new tab |
| 1283 | // at |modelIndex|. |
| 1284 | - (void)insertTabWithContents:(content::WebContents*)contents |
| 1285 | atIndex:(NSInteger)modelIndex |
| 1286 | inForeground:(bool)inForeground { |
| 1287 | DCHECK(contents); |
| 1288 | DCHECK(modelIndex == TabStripModel::kNoTab || |
| 1289 | tabStripModel_->ContainsIndex(modelIndex)); |
| 1290 | |
| 1291 | // Cancel any pending tab transition. |
| 1292 | hoverTabSelector_->CancelTabTransition(); |
| 1293 | |
| 1294 | // Take closing tabs into account. |
| 1295 | NSInteger index = [self indexFromModelIndex:modelIndex]; |
| 1296 | |
| 1297 | // Make a new tab. Load the contents of this tab from the nib and associate |
| 1298 | // the new controller with |contents| so it can be looked up later. |
Ben Murdoch | eb525c5 | 2013-07-10 11:40:50 +0100 | [diff] [blame] | 1299 | base::scoped_nsobject<TabContentsController> contentsController( |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 1300 | [[TabContentsController alloc] initWithContents:contents]); |
| 1301 | [tabContentsArray_ insertObject:contentsController atIndex:index]; |
| 1302 | |
| 1303 | // Make a new tab and add it to the strip. Keep track of its controller. |
| 1304 | TabController* newController = [self newTab]; |
| 1305 | [newController setMini:tabStripModel_->IsMiniTab(modelIndex)]; |
| 1306 | [newController setPinned:tabStripModel_->IsTabPinned(modelIndex)]; |
| 1307 | [newController setApp:tabStripModel_->IsAppTab(modelIndex)]; |
| 1308 | [newController setUrl:contents->GetURL()]; |
| 1309 | [tabArray_ insertObject:newController atIndex:index]; |
| 1310 | NSView* newView = [newController view]; |
| 1311 | |
| 1312 | // Set the originating frame to just below the strip so that it animates |
| 1313 | // upwards as it's being initially layed out. Oddly, this works while doing |
| 1314 | // something similar in |-layoutTabs| confuses the window server. |
| 1315 | [newView setFrame:NSOffsetRect([newView frame], |
| 1316 | 0, -[[self class] defaultTabHeight])]; |
| 1317 | |
| 1318 | [self setTabTitle:newController withContents:contents]; |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 1319 | [newController setProjecting:chrome::ShouldShowProjectingIndicator(contents)]; |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 1320 | |
| 1321 | // If a tab is being inserted, we can again use the entire tab strip width |
| 1322 | // for layout. |
| 1323 | availableResizeWidth_ = kUseFullAvailableWidth; |
| 1324 | |
| 1325 | // We don't need to call |-layoutTabs| if the tab will be in the foreground |
| 1326 | // because it will get called when the new tab is selected by the tab model. |
| 1327 | // Whenever |-layoutTabs| is called, it'll also add the new subview. |
| 1328 | if (!inForeground) { |
| 1329 | [self layoutTabs]; |
| 1330 | } |
| 1331 | |
| 1332 | // During normal loading, we won't yet have a favicon and we'll get |
| 1333 | // subsequent state change notifications to show the throbber, but when we're |
| 1334 | // dragging a tab out into a new window, we have to put the tab's favicon |
| 1335 | // into the right state up front as we won't be told to do it from anywhere |
| 1336 | // else. |
| 1337 | [self updateFaviconForContents:contents atIndex:modelIndex]; |
| 1338 | } |
| 1339 | |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 1340 | // Called before |contents| is deactivated. |
| 1341 | - (void)tabDeactivatedWithContents:(content::WebContents*)contents { |
| 1342 | contents->GetView()->StoreFocus(); |
| 1343 | } |
| 1344 | |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 1345 | // Called when a notification is received from the model to select a particular |
| 1346 | // tab. Swaps in the toolbar and content area associated with |newContents|. |
| 1347 | - (void)activateTabWithContents:(content::WebContents*)newContents |
| 1348 | previousContents:(content::WebContents*)oldContents |
| 1349 | atIndex:(NSInteger)modelIndex |
Torne (Richard Coles) | c2e0dbd | 2013-05-09 18:35:53 +0100 | [diff] [blame] | 1350 | reason:(int)reason { |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 1351 | // Take closing tabs into account. |
| 1352 | NSInteger activeIndex = [self indexFromModelIndex:modelIndex]; |
| 1353 | |
| 1354 | if (oldContents) { |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 1355 | int oldModelIndex = |
| 1356 | browser_->tab_strip_model()->GetIndexOfWebContents(oldContents); |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 1357 | if (oldModelIndex != -1) { // When closing a tab, the old tab may be gone. |
| 1358 | NSInteger oldIndex = [self indexFromModelIndex:oldModelIndex]; |
| 1359 | TabContentsController* oldController = |
| 1360 | [tabContentsArray_ objectAtIndex:oldIndex]; |
| 1361 | [oldController willBecomeUnselectedTab]; |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 1362 | oldContents->WasHidden(); |
| 1363 | } |
| 1364 | } |
| 1365 | |
| 1366 | // First get the vector of indices, which is allays sorted in ascending order. |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 1367 | ui::ListSelectionModel::SelectedIndices selection( |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 1368 | tabStripModel_->selection_model().selected_indices()); |
| 1369 | // Iterate through all of the tabs, selecting each as necessary. |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 1370 | ui::ListSelectionModel::SelectedIndices::iterator iter = selection.begin(); |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 1371 | int i = 0; |
| 1372 | for (TabController* current in tabArray_.get()) { |
| 1373 | BOOL selected = iter != selection.end() && |
| 1374 | [self indexFromModelIndex:*iter] == i; |
| 1375 | [current setSelected:selected]; |
| 1376 | [current setActive:i == activeIndex]; |
| 1377 | if (selected) |
| 1378 | ++iter; |
| 1379 | ++i; |
| 1380 | } |
| 1381 | |
| 1382 | // Tell the new tab contents it is about to become the selected tab. Here it |
| 1383 | // can do things like make sure the toolbar is up to date. |
| 1384 | TabContentsController* newController = |
| 1385 | [tabContentsArray_ objectAtIndex:activeIndex]; |
| 1386 | [newController willBecomeSelectedTab]; |
| 1387 | |
| 1388 | // Relayout for new tabs and to let the selected tab grow to be larger in |
| 1389 | // size than surrounding tabs if the user has many. This also raises the |
| 1390 | // selected tab to the top. |
| 1391 | [self layoutTabs]; |
| 1392 | |
| 1393 | // Swap in the contents for the new tab. |
| 1394 | [self swapInTabAtIndex:modelIndex]; |
| 1395 | |
| 1396 | if (newContents) { |
| 1397 | newContents->WasShown(); |
| 1398 | newContents->GetView()->RestoreFocus(); |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 1399 | } |
| 1400 | } |
| 1401 | |
| 1402 | - (void)tabReplacedWithContents:(content::WebContents*)newContents |
| 1403 | previousContents:(content::WebContents*)oldContents |
| 1404 | atIndex:(NSInteger)modelIndex { |
| 1405 | NSInteger index = [self indexFromModelIndex:modelIndex]; |
| 1406 | TabContentsController* oldController = |
| 1407 | [tabContentsArray_ objectAtIndex:index]; |
| 1408 | DCHECK_EQ(oldContents, [oldController webContents]); |
| 1409 | |
| 1410 | // Simply create a new TabContentsController for |newContents| and place it |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 1411 | // into the array, replacing |oldContents|. An ActiveTabChanged notification |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 1412 | // will follow, at which point we will install the new view. |
Ben Murdoch | eb525c5 | 2013-07-10 11:40:50 +0100 | [diff] [blame] | 1413 | base::scoped_nsobject<TabContentsController> newController( |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 1414 | [[TabContentsController alloc] initWithContents:newContents]); |
| 1415 | |
| 1416 | // Bye bye, |oldController|. |
| 1417 | [tabContentsArray_ replaceObjectAtIndex:index withObject:newController]; |
| 1418 | |
| 1419 | // Fake a tab changed notification to force tab titles and favicons to update. |
| 1420 | [self tabChangedWithContents:newContents |
| 1421 | atIndex:modelIndex |
| 1422 | changeType:TabStripModelObserver::ALL]; |
| 1423 | } |
| 1424 | |
| 1425 | // Remove all knowledge about this tab and its associated controller, and remove |
| 1426 | // the view from the strip. |
| 1427 | - (void)removeTab:(TabController*)controller { |
| 1428 | // Cancel any pending tab transition. |
| 1429 | hoverTabSelector_->CancelTabTransition(); |
| 1430 | |
| 1431 | NSUInteger index = [tabArray_ indexOfObject:controller]; |
| 1432 | |
| 1433 | // Release the tab contents controller so those views get destroyed. This |
| 1434 | // will remove all the tab content Cocoa views from the hierarchy. A |
| 1435 | // subsequent "select tab" notification will follow from the model. To |
| 1436 | // tell us what to swap in in its absence. |
| 1437 | [tabContentsArray_ removeObjectAtIndex:index]; |
| 1438 | |
| 1439 | // Remove the view from the tab strip. |
| 1440 | NSView* tab = [controller view]; |
| 1441 | [tab removeFromSuperview]; |
| 1442 | |
| 1443 | // Remove ourself as an observer. |
| 1444 | [[NSNotificationCenter defaultCenter] |
| 1445 | removeObserver:self |
| 1446 | name:NSViewDidUpdateTrackingAreasNotification |
| 1447 | object:tab]; |
| 1448 | |
| 1449 | // Clear the tab controller's target. |
| 1450 | // TODO(viettrungluu): [crbug.com/23829] Find a better way to handle the tab |
| 1451 | // controller's target. |
| 1452 | [controller setTarget:nil]; |
| 1453 | |
| 1454 | if ([hoveredTab_ isEqual:tab]) |
| 1455 | hoveredTab_ = nil; |
| 1456 | |
| 1457 | NSValue* identifier = [NSValue valueWithPointer:tab]; |
| 1458 | [targetFrames_ removeObjectForKey:identifier]; |
| 1459 | |
| 1460 | // Once we're totally done with the tab, delete its controller |
| 1461 | [tabArray_ removeObjectAtIndex:index]; |
| 1462 | } |
| 1463 | |
| 1464 | // Called by the CAAnimation delegate when the tab completes the closing |
| 1465 | // animation. |
| 1466 | - (void)animationDidStopForController:(TabController*)controller |
| 1467 | finished:(BOOL)finished { |
| 1468 | [closingControllers_ removeObject:controller]; |
| 1469 | [self removeTab:controller]; |
| 1470 | } |
| 1471 | |
| 1472 | // Save off which TabController is closing and tell its view's animator |
| 1473 | // where to move the tab to. Registers a delegate to call back when the |
| 1474 | // animation is complete in order to remove the tab from the model. |
| 1475 | - (void)startClosingTabWithAnimation:(TabController*)closingTab { |
| 1476 | DCHECK([NSThread isMainThread]); |
| 1477 | |
| 1478 | // Cancel any pending tab transition. |
| 1479 | hoverTabSelector_->CancelTabTransition(); |
| 1480 | |
| 1481 | // Save off the controller into the set of animating tabs. This alerts |
| 1482 | // the layout method to not do anything with it and allows us to correctly |
| 1483 | // calculate offsets when working with indices into the model. |
| 1484 | [closingControllers_ addObject:closingTab]; |
| 1485 | |
| 1486 | // Mark the tab as closing. This prevents it from generating any drags or |
| 1487 | // selections while it's animating closed. |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 1488 | [[closingTab tabView] setClosing:YES]; |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 1489 | |
| 1490 | // Register delegate (owned by the animation system). |
| 1491 | NSView* tabView = [closingTab view]; |
| 1492 | CAAnimation* animation = [[tabView animationForKey:@"frameOrigin"] copy]; |
| 1493 | [animation autorelease]; |
Ben Murdoch | eb525c5 | 2013-07-10 11:40:50 +0100 | [diff] [blame] | 1494 | base::scoped_nsobject<TabCloseAnimationDelegate> delegate( |
| 1495 | [[TabCloseAnimationDelegate alloc] initWithTabStrip:self |
| 1496 | tabController:closingTab]); |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 1497 | [animation setDelegate:delegate.get()]; // Retains delegate. |
| 1498 | NSMutableDictionary* animationDictionary = |
| 1499 | [NSMutableDictionary dictionaryWithDictionary:[tabView animations]]; |
| 1500 | [animationDictionary setObject:animation forKey:@"frameOrigin"]; |
| 1501 | [tabView setAnimations:animationDictionary]; |
| 1502 | |
| 1503 | // Periscope down! Animate the tab. |
| 1504 | NSRect newFrame = [tabView frame]; |
| 1505 | newFrame = NSOffsetRect(newFrame, 0, -newFrame.size.height); |
| 1506 | ScopedNSAnimationContextGroup animationGroup(true); |
| 1507 | animationGroup.SetCurrentContextDuration(kAnimationDuration); |
| 1508 | [[tabView animator] setFrame:newFrame]; |
| 1509 | } |
| 1510 | |
| 1511 | // Called when a notification is received from the model that the given tab |
| 1512 | // has gone away. Start an animation then force a layout to put everything |
| 1513 | // in motion. |
| 1514 | - (void)tabDetachedWithContents:(content::WebContents*)contents |
| 1515 | atIndex:(NSInteger)modelIndex { |
| 1516 | // Take closing tabs into account. |
| 1517 | NSInteger index = [self indexFromModelIndex:modelIndex]; |
| 1518 | |
| 1519 | // Cancel any pending tab transition. |
| 1520 | hoverTabSelector_->CancelTabTransition(); |
| 1521 | |
| 1522 | TabController* tab = [tabArray_ objectAtIndex:index]; |
| 1523 | if (tabStripModel_->count() > 0) { |
| 1524 | [self startClosingTabWithAnimation:tab]; |
| 1525 | [self layoutTabs]; |
| 1526 | } else { |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 1527 | // Don't remove the tab, as that makes the window look jarring without any |
| 1528 | // tabs. Instead, simply mark it as closing to prevent the tab from |
| 1529 | // generating any drags or selections. |
| 1530 | [[tab tabView] setClosing:YES]; |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 1531 | } |
| 1532 | |
| 1533 | [delegate_ onTabDetachedWithContents:contents]; |
| 1534 | } |
| 1535 | |
| 1536 | // A helper routine for creating an NSImageView to hold the favicon or app icon |
| 1537 | // for |contents|. |
| 1538 | - (NSImageView*)iconImageViewForContents:(content::WebContents*)contents { |
| 1539 | extensions::TabHelper* extensions_tab_helper = |
| 1540 | extensions::TabHelper::FromWebContents(contents); |
| 1541 | BOOL isApp = extensions_tab_helper->is_app(); |
| 1542 | NSImage* image = nil; |
| 1543 | // Favicons come from the renderer, and the renderer draws everything in the |
| 1544 | // system color space. |
| 1545 | CGColorSpaceRef colorSpace = base::mac::GetSystemColorSpace(); |
| 1546 | if (isApp) { |
| 1547 | SkBitmap* icon = extensions_tab_helper->GetExtensionAppIcon(); |
| 1548 | if (icon) |
| 1549 | image = gfx::SkBitmapToNSImageWithColorSpace(*icon, colorSpace); |
| 1550 | } else { |
| 1551 | image = mac::FaviconForWebContents(contents); |
| 1552 | } |
| 1553 | |
| 1554 | // Either we don't have a valid favicon or there was some issue converting it |
| 1555 | // from an SkBitmap. Either way, just show the default. |
| 1556 | if (!image) |
| 1557 | image = defaultFavicon_.get(); |
| 1558 | NSRect frame = NSMakeRect(0, 0, kIconWidthAndHeight, kIconWidthAndHeight); |
| 1559 | NSImageView* view = [[[NSImageView alloc] initWithFrame:frame] autorelease]; |
| 1560 | [view setImage:image]; |
| 1561 | return view; |
| 1562 | } |
| 1563 | |
| 1564 | // Updates the current loading state, replacing the icon view with a favicon, |
| 1565 | // a throbber, the default icon, or nothing at all. |
| 1566 | - (void)updateFaviconForContents:(content::WebContents*)contents |
| 1567 | atIndex:(NSInteger)modelIndex { |
| 1568 | if (!contents) |
| 1569 | return; |
| 1570 | |
| 1571 | static NSImage* throbberWaitingImage = |
| 1572 | ResourceBundle::GetSharedInstance().GetNativeImageNamed( |
| 1573 | IDR_THROBBER_WAITING).CopyNSImage(); |
| 1574 | static NSImage* throbberLoadingImage = |
| 1575 | ResourceBundle::GetSharedInstance().GetNativeImageNamed( |
| 1576 | IDR_THROBBER).CopyNSImage(); |
| 1577 | static NSImage* sadFaviconImage = |
| 1578 | ResourceBundle::GetSharedInstance().GetNativeImageNamed( |
| 1579 | IDR_SAD_FAVICON).CopyNSImage(); |
| 1580 | |
| 1581 | // Take closing tabs into account. |
| 1582 | NSInteger index = [self indexFromModelIndex:modelIndex]; |
| 1583 | TabController* tabController = [tabArray_ objectAtIndex:index]; |
| 1584 | |
| 1585 | FaviconTabHelper* favicon_tab_helper = |
| 1586 | FaviconTabHelper::FromWebContents(contents); |
| 1587 | bool oldHasIcon = [tabController iconView] != nil; |
| 1588 | bool newHasIcon = favicon_tab_helper->ShouldDisplayFavicon() || |
| 1589 | tabStripModel_->IsMiniTab(modelIndex); // Always show icon if mini. |
| 1590 | |
| 1591 | TabLoadingState oldState = [tabController loadingState]; |
| 1592 | TabLoadingState newState = kTabDone; |
| 1593 | NSImage* throbberImage = nil; |
| 1594 | if (contents->IsCrashed()) { |
| 1595 | newState = kTabCrashed; |
| 1596 | newHasIcon = true; |
| 1597 | } else if (contents->IsWaitingForResponse()) { |
| 1598 | newState = kTabWaiting; |
| 1599 | throbberImage = throbberWaitingImage; |
| 1600 | } else if (contents->IsLoading()) { |
| 1601 | newState = kTabLoading; |
| 1602 | throbberImage = throbberLoadingImage; |
| 1603 | } |
| 1604 | |
| 1605 | if (oldState != newState) |
| 1606 | [tabController setLoadingState:newState]; |
| 1607 | |
| 1608 | // While loading, this function is called repeatedly with the same state. |
| 1609 | // To avoid expensive unnecessary view manipulation, only make changes when |
| 1610 | // the state is actually changing. When loading is complete (kTabDone), |
| 1611 | // every call to this function is significant. |
| 1612 | if (newState == kTabDone || oldState != newState || |
| 1613 | oldHasIcon != newHasIcon) { |
| 1614 | NSView* iconView = nil; |
| 1615 | if (newHasIcon) { |
| 1616 | if (newState == kTabDone) { |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 1617 | NSImageView* imageView = [self iconImageViewForContents:contents]; |
| 1618 | TabAudioIndicatorViewMac* tabAudioIndicatorViewMac = |
| 1619 | base::mac::ObjCCast<TabAudioIndicatorViewMac>( |
| 1620 | [tabController iconView]); |
| 1621 | |
| 1622 | ui::ThemeProvider* theme = [[tabStripView_ window] themeProvider]; |
| 1623 | if (theme && [tabController projecting]) { |
| 1624 | NSImage* projectorGlow = |
Ben Murdoch | bb1529c | 2013-08-08 10:24:53 +0100 | [diff] [blame^] | 1625 | theme->GetNSImageNamed(IDR_TAB_CAPTURE_GLOW); |
| 1626 | NSImage* projector = theme->GetNSImageNamed(IDR_TAB_CAPTURE); |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 1627 | |
| 1628 | NSRect frame = NSMakeRect(0, |
| 1629 | 0, |
| 1630 | kProjectingIconWidthAndHeight, |
| 1631 | kProjectingIconWidthAndHeight); |
| 1632 | TabProjectingImageView* projectingView = |
Torne (Richard Coles) | c2e0dbd | 2013-05-09 18:35:53 +0100 | [diff] [blame] | 1633 | [[[TabProjectingImageView alloc] |
| 1634 | initWithFrame:frame |
| 1635 | backgroundImage:[imageView image] |
| 1636 | projectorImage:projector |
| 1637 | throbImage:projectorGlow |
| 1638 | durationMS:kRecordingDurationMs |
Ben Murdoch | eb525c5 | 2013-07-10 11:40:50 +0100 | [diff] [blame] | 1639 | animationContainer:animationContainer_.get()] autorelease]; |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 1640 | |
| 1641 | iconView = projectingView; |
| 1642 | } else if (theme && chrome::ShouldShowRecordingIndicator(contents)) { |
| 1643 | // Create a masked favicon. |
Ben Murdoch | bb1529c | 2013-08-08 10:24:53 +0100 | [diff] [blame^] | 1644 | NSImage* mask = theme->GetNSImageNamed(IDR_TAB_RECORDING_MASK); |
| 1645 | NSImage* recording = theme->GetNSImageNamed(IDR_TAB_RECORDING); |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 1646 | NSImage* favIconMasked = CreateMaskedFaviconForRecording( |
| 1647 | [imageView image], mask, recording); |
| 1648 | |
| 1649 | NSRect frame = |
| 1650 | NSMakeRect(0, 0, kIconWidthAndHeight, kIconWidthAndHeight); |
| 1651 | ThrobbingImageView* recordingView = |
| 1652 | [[[ThrobbingImageView alloc] |
| 1653 | initWithFrame:frame |
| 1654 | backgroundImage:favIconMasked |
| 1655 | throbImage:recording |
| 1656 | durationMS:kRecordingDurationMs |
Torne (Richard Coles) | c2e0dbd | 2013-05-09 18:35:53 +0100 | [diff] [blame] | 1657 | throbPosition:kThrobPositionBottomRight |
Ben Murdoch | eb525c5 | 2013-07-10 11:40:50 +0100 | [diff] [blame] | 1658 | animationContainer:animationContainer_.get()] autorelease]; |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 1659 | |
| 1660 | iconView = recordingView; |
| 1661 | } else if (chrome::IsPlayingAudio(contents) || |
| 1662 | [tabAudioIndicatorViewMac isAnimating]) { |
| 1663 | if (!tabAudioIndicatorViewMac) { |
| 1664 | NSRect frame = |
| 1665 | NSMakeRect(0, 0, kIconWidthAndHeight, kIconWidthAndHeight); |
| 1666 | tabAudioIndicatorViewMac = [[[TabAudioIndicatorViewMac alloc] |
| 1667 | initWithFrame:frame] autorelease]; |
Torne (Richard Coles) | c2e0dbd | 2013-05-09 18:35:53 +0100 | [diff] [blame] | 1668 | [tabAudioIndicatorViewMac |
Ben Murdoch | eb525c5 | 2013-07-10 11:40:50 +0100 | [diff] [blame] | 1669 | setAnimationContainer:animationContainer_.get()]; |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 1670 | } |
| 1671 | [tabAudioIndicatorViewMac |
| 1672 | setIsPlayingAudio:chrome::IsPlayingAudio(contents)]; |
| 1673 | [tabAudioIndicatorViewMac setBackgroundImage:[imageView image]]; |
| 1674 | iconView = tabAudioIndicatorViewMac; |
| 1675 | } else { |
| 1676 | iconView = imageView; |
| 1677 | } |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 1678 | } else if (newState == kTabCrashed) { |
| 1679 | NSImage* oldImage = [[self iconImageViewForContents:contents] image]; |
| 1680 | NSRect frame = |
| 1681 | NSMakeRect(0, 0, kIconWidthAndHeight, kIconWidthAndHeight); |
| 1682 | iconView = [ThrobberView toastThrobberViewWithFrame:frame |
| 1683 | beforeImage:oldImage |
| 1684 | afterImage:sadFaviconImage]; |
| 1685 | } else { |
| 1686 | NSRect frame = |
| 1687 | NSMakeRect(0, 0, kIconWidthAndHeight, kIconWidthAndHeight); |
| 1688 | iconView = [ThrobberView filmstripThrobberViewWithFrame:frame |
| 1689 | image:throbberImage]; |
| 1690 | } |
| 1691 | } |
| 1692 | |
| 1693 | [tabController setIconView:iconView]; |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 1694 | if (iconView && ![tabController projecting]) { |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 1695 | // See the comment above kTabOverlap for why these DCHECKs exist. |
| 1696 | DCHECK_GE(NSMinX([iconView frame]), kTabOverlap); |
| 1697 | // TODO(thakis): Ideally, this would be true too, but it's not true in |
| 1698 | // some tests. |
| 1699 | //DCHECK_LE(NSMaxX([iconView frame]), |
| 1700 | // NSWidth([[tabController view] frame]) - kTabOverlap); |
| 1701 | } |
| 1702 | } |
| 1703 | } |
| 1704 | |
| 1705 | // Called when a notification is received from the model that the given tab |
| 1706 | // has been updated. |loading| will be YES when we only want to update the |
| 1707 | // throbber state, not anything else about the (partially) loading tab. |
| 1708 | - (void)tabChangedWithContents:(content::WebContents*)contents |
| 1709 | atIndex:(NSInteger)modelIndex |
| 1710 | changeType:(TabStripModelObserver::TabChangeType)change { |
| 1711 | // Take closing tabs into account. |
| 1712 | NSInteger index = [self indexFromModelIndex:modelIndex]; |
| 1713 | |
| 1714 | if (modelIndex == tabStripModel_->active_index()) |
| 1715 | [delegate_ onTabChanged:change withContents:contents]; |
| 1716 | |
| 1717 | if (change == TabStripModelObserver::TITLE_NOT_LOADING) { |
| 1718 | // TODO(sky): make this work. |
| 1719 | // We'll receive another notification of the change asynchronously. |
| 1720 | return; |
| 1721 | } |
| 1722 | |
| 1723 | TabController* tabController = [tabArray_ objectAtIndex:index]; |
| 1724 | |
| 1725 | if (change != TabStripModelObserver::LOADING_ONLY) |
| 1726 | [self setTabTitle:tabController withContents:contents]; |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 1727 | [tabController setProjecting:chrome::ShouldShowProjectingIndicator(contents)]; |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 1728 | |
| 1729 | [self updateFaviconForContents:contents atIndex:modelIndex]; |
| 1730 | |
| 1731 | TabContentsController* updatedController = |
| 1732 | [tabContentsArray_ objectAtIndex:index]; |
| 1733 | [updatedController tabDidChange:contents]; |
| 1734 | } |
| 1735 | |
| 1736 | // Called when a tab is moved (usually by drag&drop). Keep our parallel arrays |
| 1737 | // in sync with the tab strip model. It can also be pinned/unpinned |
| 1738 | // simultaneously, so we need to take care of that. |
| 1739 | - (void)tabMovedWithContents:(content::WebContents*)contents |
| 1740 | fromIndex:(NSInteger)modelFrom |
| 1741 | toIndex:(NSInteger)modelTo { |
| 1742 | // Take closing tabs into account. |
| 1743 | NSInteger from = [self indexFromModelIndex:modelFrom]; |
| 1744 | NSInteger to = [self indexFromModelIndex:modelTo]; |
| 1745 | |
| 1746 | // Cancel any pending tab transition. |
| 1747 | hoverTabSelector_->CancelTabTransition(); |
| 1748 | |
Ben Murdoch | eb525c5 | 2013-07-10 11:40:50 +0100 | [diff] [blame] | 1749 | base::scoped_nsobject<TabContentsController> movedTabContentsController( |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 1750 | [[tabContentsArray_ objectAtIndex:from] retain]); |
| 1751 | [tabContentsArray_ removeObjectAtIndex:from]; |
| 1752 | [tabContentsArray_ insertObject:movedTabContentsController.get() |
| 1753 | atIndex:to]; |
Ben Murdoch | eb525c5 | 2013-07-10 11:40:50 +0100 | [diff] [blame] | 1754 | base::scoped_nsobject<TabController> movedTabController( |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 1755 | [[tabArray_ objectAtIndex:from] retain]); |
| 1756 | DCHECK([movedTabController isKindOfClass:[TabController class]]); |
| 1757 | [tabArray_ removeObjectAtIndex:from]; |
| 1758 | [tabArray_ insertObject:movedTabController.get() atIndex:to]; |
| 1759 | |
| 1760 | // The tab moved, which means that the mini-tab state may have changed. |
| 1761 | if (tabStripModel_->IsMiniTab(modelTo) != [movedTabController mini]) |
| 1762 | [self tabMiniStateChangedWithContents:contents atIndex:modelTo]; |
| 1763 | |
| 1764 | [self layoutTabs]; |
| 1765 | } |
| 1766 | |
| 1767 | // Called when a tab is pinned or unpinned without moving. |
| 1768 | - (void)tabMiniStateChangedWithContents:(content::WebContents*)contents |
| 1769 | atIndex:(NSInteger)modelIndex { |
| 1770 | // Take closing tabs into account. |
| 1771 | NSInteger index = [self indexFromModelIndex:modelIndex]; |
| 1772 | |
| 1773 | TabController* tabController = [tabArray_ objectAtIndex:index]; |
| 1774 | DCHECK([tabController isKindOfClass:[TabController class]]); |
| 1775 | |
| 1776 | // Don't do anything if the change was already picked up by the move event. |
| 1777 | if (tabStripModel_->IsMiniTab(modelIndex) == [tabController mini]) |
| 1778 | return; |
| 1779 | |
| 1780 | [tabController setMini:tabStripModel_->IsMiniTab(modelIndex)]; |
| 1781 | [tabController setPinned:tabStripModel_->IsTabPinned(modelIndex)]; |
| 1782 | [tabController setApp:tabStripModel_->IsAppTab(modelIndex)]; |
| 1783 | [tabController setUrl:contents->GetURL()]; |
| 1784 | [self updateFaviconForContents:contents atIndex:modelIndex]; |
| 1785 | // If the tab is being restored and it's pinned, the mini state is set after |
| 1786 | // the tab has already been rendered, so re-layout the tabstrip. In all other |
| 1787 | // cases, the state is set before the tab is rendered so this isn't needed. |
| 1788 | [self layoutTabs]; |
| 1789 | } |
| 1790 | |
| 1791 | - (void)setFrameOfActiveTab:(NSRect)frame { |
| 1792 | NSView* view = [self activeTabView]; |
| 1793 | NSValue* identifier = [NSValue valueWithPointer:view]; |
| 1794 | [targetFrames_ setObject:[NSValue valueWithRect:frame] |
| 1795 | forKey:identifier]; |
| 1796 | [view setFrame:frame]; |
| 1797 | } |
| 1798 | |
| 1799 | - (TabStripModel*)tabStripModel { |
| 1800 | return tabStripModel_; |
| 1801 | } |
| 1802 | |
| 1803 | - (NSView*)activeTabView { |
| 1804 | int activeIndex = tabStripModel_->active_index(); |
| 1805 | // Take closing tabs into account. They can't ever be selected. |
| 1806 | activeIndex = [self indexFromModelIndex:activeIndex]; |
| 1807 | return [self viewAtIndex:activeIndex]; |
| 1808 | } |
| 1809 | |
| 1810 | // Find the model index based on the x coordinate of the placeholder. If there |
| 1811 | // is no placeholder, this returns the end of the tab strip. Closing tabs are |
| 1812 | // not considered in computing the index. |
| 1813 | - (int)indexOfPlaceholder { |
| 1814 | double placeholderX = placeholderFrame_.origin.x; |
| 1815 | int index = 0; |
| 1816 | int location = 0; |
| 1817 | // Use |tabArray_| here instead of the tab strip count in order to get the |
| 1818 | // correct index when there are closing tabs to the left of the placeholder. |
| 1819 | const int count = [tabArray_ count]; |
| 1820 | while (index < count) { |
| 1821 | // Ignore closing tabs for simplicity. The only drawback of this is that |
| 1822 | // if the placeholder is placed right before one or several contiguous |
| 1823 | // currently closing tabs, the associated TabController will start at the |
| 1824 | // end of the closing tabs. |
| 1825 | if ([closingControllers_ containsObject:[tabArray_ objectAtIndex:index]]) { |
| 1826 | index++; |
| 1827 | continue; |
| 1828 | } |
| 1829 | NSView* curr = [self viewAtIndex:index]; |
| 1830 | // The placeholder tab works by changing the frame of the tab being dragged |
| 1831 | // to be the bounds of the placeholder, so we need to skip it while we're |
| 1832 | // iterating, otherwise we'll end up off by one. Note This only effects |
| 1833 | // dragging to the right, not to the left. |
| 1834 | if (curr == placeholderTab_) { |
| 1835 | index++; |
| 1836 | continue; |
| 1837 | } |
| 1838 | if (placeholderX <= NSMinX([curr frame])) |
| 1839 | break; |
| 1840 | index++; |
| 1841 | location++; |
| 1842 | } |
| 1843 | return location; |
| 1844 | } |
| 1845 | |
| 1846 | // Move the given tab at index |from| in this window to the location of the |
| 1847 | // current placeholder. |
| 1848 | - (void)moveTabFromIndex:(NSInteger)from { |
| 1849 | int toIndex = [self indexOfPlaceholder]; |
| 1850 | // Cancel any pending tab transition. |
| 1851 | hoverTabSelector_->CancelTabTransition(); |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 1852 | tabStripModel_->MoveWebContentsAt(from, toIndex, true); |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 1853 | } |
| 1854 | |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 1855 | // Drop a given WebContents at the location of the current placeholder. |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 1856 | // If there is no placeholder, it will go at the end. Used when dragging from |
| 1857 | // another window when we don't have access to the WebContents as part of our |
| 1858 | // strip. |frame| is in the coordinate system of the tab strip view and |
| 1859 | // represents where the user dropped the new tab so it can be animated into its |
| 1860 | // correct location when the tab is added to the model. If the tab was pinned in |
| 1861 | // its previous window, setting |pinned| to YES will propagate that state to the |
| 1862 | // new window. Mini-tabs are either app or pinned tabs; the app state is stored |
| 1863 | // by the |contents|, but the |pinned| state is the caller's responsibility. |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 1864 | - (void)dropWebContents:(WebContents*)contents |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 1865 | withFrame:(NSRect)frame |
| 1866 | asPinnedTab:(BOOL)pinned { |
| 1867 | int modelIndex = [self indexOfPlaceholder]; |
| 1868 | |
| 1869 | // Mark that the new tab being created should start at |frame|. It will be |
| 1870 | // reset as soon as the tab has been positioned. |
| 1871 | droppedTabFrame_ = frame; |
| 1872 | |
| 1873 | // Insert it into this tab strip. We want it in the foreground and to not |
| 1874 | // inherit the current tab's group. |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 1875 | tabStripModel_->InsertWebContentsAt( |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 1876 | modelIndex, contents, |
| 1877 | TabStripModel::ADD_ACTIVE | (pinned ? TabStripModel::ADD_PINNED : 0)); |
| 1878 | } |
| 1879 | |
| 1880 | // Called when the tab strip view changes size. As we only registered for |
| 1881 | // changes on our view, we know it's only for our view. Layout w/out |
| 1882 | // animations since they are blocked by the resize nested runloop. We need |
| 1883 | // the views to adjust immediately. Neither the tabs nor their z-order are |
| 1884 | // changed, so we don't need to update the subviews. |
| 1885 | - (void)tabViewFrameChanged:(NSNotification*)info { |
| 1886 | [self layoutTabsWithAnimation:NO regenerateSubviews:NO]; |
| 1887 | } |
| 1888 | |
| 1889 | // Called when the tracking areas for any given tab are updated. This allows |
| 1890 | // the individual tabs to update their hover states correctly. |
| 1891 | // Only generates the event if the cursor is in the tab strip. |
| 1892 | - (void)tabUpdateTracking:(NSNotification*)notification { |
| 1893 | DCHECK([[notification object] isKindOfClass:[TabView class]]); |
| 1894 | DCHECK(mouseInside_); |
| 1895 | NSWindow* window = [tabStripView_ window]; |
| 1896 | NSPoint location = [window mouseLocationOutsideOfEventStream]; |
| 1897 | if (NSPointInRect(location, [tabStripView_ frame])) { |
| 1898 | NSEvent* mouseEvent = [NSEvent mouseEventWithType:NSMouseMoved |
| 1899 | location:location |
| 1900 | modifierFlags:0 |
| 1901 | timestamp:0 |
| 1902 | windowNumber:[window windowNumber] |
| 1903 | context:nil |
| 1904 | eventNumber:0 |
| 1905 | clickCount:0 |
| 1906 | pressure:0]; |
| 1907 | [self mouseMoved:mouseEvent]; |
| 1908 | } |
| 1909 | } |
| 1910 | |
| 1911 | - (BOOL)inRapidClosureMode { |
| 1912 | return availableResizeWidth_ != kUseFullAvailableWidth; |
| 1913 | } |
| 1914 | |
| 1915 | // Disable tab dragging when there are any pending animations. |
| 1916 | - (BOOL)tabDraggingAllowed { |
| 1917 | return [closingControllers_ count] == 0; |
| 1918 | } |
| 1919 | |
| 1920 | - (void)mouseMoved:(NSEvent*)event { |
| 1921 | // Use hit test to figure out what view we are hovering over. |
| 1922 | NSView* targetView = [tabStripView_ hitTest:[event locationInWindow]]; |
| 1923 | |
| 1924 | // Set the new tab button hover state iff the mouse is over the button. |
| 1925 | BOOL shouldShowHoverImage = [targetView isKindOfClass:[NewTabButton class]]; |
| 1926 | [self setNewTabButtonHoverState:shouldShowHoverImage]; |
| 1927 | |
| 1928 | TabView* tabView = (TabView*)targetView; |
| 1929 | if (![tabView isKindOfClass:[TabView class]]) { |
| 1930 | if ([[tabView superview] isKindOfClass:[TabView class]]) { |
| 1931 | tabView = (TabView*)[targetView superview]; |
| 1932 | } else { |
| 1933 | tabView = nil; |
| 1934 | } |
| 1935 | } |
| 1936 | |
| 1937 | if (hoveredTab_ != tabView) { |
| 1938 | [hoveredTab_ mouseExited:nil]; // We don't pass event because moved events |
| 1939 | [tabView mouseEntered:nil]; // don't have valid tracking areas |
| 1940 | hoveredTab_ = tabView; |
| 1941 | } else { |
| 1942 | [hoveredTab_ mouseMoved:event]; |
| 1943 | } |
| 1944 | } |
| 1945 | |
| 1946 | - (void)mouseEntered:(NSEvent*)event { |
| 1947 | NSTrackingArea* area = [event trackingArea]; |
| 1948 | if ([area isEqual:trackingArea_]) { |
| 1949 | mouseInside_ = YES; |
| 1950 | [self setTabTrackingAreasEnabled:YES]; |
| 1951 | [self mouseMoved:event]; |
| 1952 | } |
| 1953 | } |
| 1954 | |
| 1955 | // Called when the tracking area is in effect which means we're tracking to |
| 1956 | // see if the user leaves the tab strip with their mouse. When they do, |
| 1957 | // reset layout to use all available width. |
| 1958 | - (void)mouseExited:(NSEvent*)event { |
| 1959 | NSTrackingArea* area = [event trackingArea]; |
| 1960 | if ([area isEqual:trackingArea_]) { |
| 1961 | mouseInside_ = NO; |
| 1962 | [self setTabTrackingAreasEnabled:NO]; |
| 1963 | availableResizeWidth_ = kUseFullAvailableWidth; |
| 1964 | [hoveredTab_ mouseExited:event]; |
| 1965 | hoveredTab_ = nil; |
| 1966 | [self layoutTabs]; |
| 1967 | } else if ([area isEqual:newTabTrackingArea_]) { |
| 1968 | // If the mouse is moved quickly enough, it is possible for the mouse to |
| 1969 | // leave the tabstrip without sending any mouseMoved: messages at all. |
| 1970 | // Since this would result in the new tab button incorrectly staying in the |
| 1971 | // hover state, disable the hover image on every mouse exit. |
| 1972 | [self setNewTabButtonHoverState:NO]; |
| 1973 | } |
| 1974 | } |
| 1975 | |
| 1976 | // Enable/Disable the tracking areas for the tabs. They are only enabled |
| 1977 | // when the mouse is in the tabstrip. |
| 1978 | - (void)setTabTrackingAreasEnabled:(BOOL)enabled { |
| 1979 | NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter]; |
| 1980 | for (TabController* controller in tabArray_.get()) { |
| 1981 | TabView* tabView = [controller tabView]; |
| 1982 | if (enabled) { |
| 1983 | // Set self up to observe tabs so hover states will be correct. |
| 1984 | [defaultCenter addObserver:self |
| 1985 | selector:@selector(tabUpdateTracking:) |
| 1986 | name:NSViewDidUpdateTrackingAreasNotification |
| 1987 | object:tabView]; |
| 1988 | } else { |
| 1989 | [defaultCenter removeObserver:self |
| 1990 | name:NSViewDidUpdateTrackingAreasNotification |
| 1991 | object:tabView]; |
| 1992 | } |
| 1993 | [tabView setTrackingEnabled:enabled]; |
| 1994 | } |
| 1995 | } |
| 1996 | |
| 1997 | // Sets the new tab button's image based on the current hover state. Does |
| 1998 | // nothing if the hover state is already correct. |
| 1999 | - (void)setNewTabButtonHoverState:(BOOL)shouldShowHover { |
| 2000 | if (shouldShowHover && !newTabButtonShowingHoverImage_) { |
| 2001 | newTabButtonShowingHoverImage_ = YES; |
| 2002 | [[newTabButton_ cell] setIsMouseInside:YES]; |
| 2003 | } else if (!shouldShowHover && newTabButtonShowingHoverImage_) { |
| 2004 | newTabButtonShowingHoverImage_ = NO; |
| 2005 | [[newTabButton_ cell] setIsMouseInside:NO]; |
| 2006 | } |
| 2007 | } |
| 2008 | |
| 2009 | // Adds the given subview to (the end of) the list of permanent subviews |
| 2010 | // (specified from bottom up). These subviews will always be below the |
| 2011 | // transitory subviews (tabs). |-regenerateSubviewList| must be called to |
| 2012 | // effectuate the addition. |
| 2013 | - (void)addSubviewToPermanentList:(NSView*)aView { |
| 2014 | if (aView) |
| 2015 | [permanentSubviews_ addObject:aView]; |
| 2016 | } |
| 2017 | |
| 2018 | // Update the subviews, keeping the permanent ones (or, more correctly, putting |
| 2019 | // in the ones listed in permanentSubviews_), and putting in the current tabs in |
| 2020 | // the correct z-order. Any current subviews which is neither in the permanent |
| 2021 | // list nor a (current) tab will be removed. So if you add such a subview, you |
| 2022 | // should call |-addSubviewToPermanentList:| (or better yet, call that and then |
| 2023 | // |-regenerateSubviewList| to actually add it). |
| 2024 | - (void)regenerateSubviewList { |
| 2025 | // Remove self as an observer from all the old tabs before a new set of |
| 2026 | // potentially different tabs is put in place. |
| 2027 | [self setTabTrackingAreasEnabled:NO]; |
| 2028 | |
| 2029 | // Subviews to put in (in bottom-to-top order), beginning with the permanent |
| 2030 | // ones. |
| 2031 | NSMutableArray* subviews = [NSMutableArray arrayWithArray:permanentSubviews_]; |
| 2032 | |
| 2033 | NSView* activeTabView = nil; |
| 2034 | // Go through tabs in reverse order, since |subviews| is bottom-to-top. |
| 2035 | for (TabController* tab in [tabArray_ reverseObjectEnumerator]) { |
| 2036 | NSView* tabView = [tab view]; |
| 2037 | if ([tab active]) { |
| 2038 | DCHECK(!activeTabView); |
| 2039 | activeTabView = tabView; |
| 2040 | } else { |
| 2041 | [subviews addObject:tabView]; |
| 2042 | } |
| 2043 | } |
| 2044 | if (activeTabView) { |
| 2045 | [subviews addObject:activeTabView]; |
| 2046 | } |
Ben Murdoch | eb525c5 | 2013-07-10 11:40:50 +0100 | [diff] [blame] | 2047 | WithNoAnimation noAnimation; |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 2048 | [tabStripView_ setSubviews:subviews]; |
| 2049 | [self setTabTrackingAreasEnabled:mouseInside_]; |
| 2050 | } |
| 2051 | |
| 2052 | // Get the index and disposition for a potential URL(s) drop given a point (in |
| 2053 | // the |TabStripView|'s coordinates). It considers only the x-coordinate of the |
| 2054 | // given point. If it's in the "middle" of a tab, it drops on that tab. If it's |
| 2055 | // to the left, it inserts to the left, and similarly for the right. |
| 2056 | - (void)droppingURLsAt:(NSPoint)point |
| 2057 | givesIndex:(NSInteger*)index |
| 2058 | disposition:(WindowOpenDisposition*)disposition { |
| 2059 | // Proportion of the tab which is considered the "middle" (and causes things |
| 2060 | // to drop on that tab). |
| 2061 | const double kMiddleProportion = 0.5; |
| 2062 | const double kLRProportion = (1.0 - kMiddleProportion) / 2.0; |
| 2063 | |
| 2064 | DCHECK(index && disposition); |
| 2065 | NSInteger i = 0; |
| 2066 | for (TabController* tab in tabArray_.get()) { |
| 2067 | NSView* view = [tab view]; |
| 2068 | DCHECK([view isKindOfClass:[TabView class]]); |
| 2069 | |
| 2070 | // Recall that |-[NSView frame]| is in its superview's coordinates, so a |
| 2071 | // |TabView|'s frame is in the coordinates of the |TabStripView| (which |
| 2072 | // matches the coordinate system of |point|). |
| 2073 | NSRect frame = [view frame]; |
| 2074 | |
| 2075 | // Modify the frame to make it "unoverlapped". |
| 2076 | frame.origin.x += kTabOverlap / 2.0; |
| 2077 | frame.size.width -= kTabOverlap; |
| 2078 | if (frame.size.width < 1.0) |
| 2079 | frame.size.width = 1.0; // try to avoid complete failure |
| 2080 | |
| 2081 | // Drop in a new tab to the left of tab |i|? |
| 2082 | if (point.x < (frame.origin.x + kLRProportion * frame.size.width)) { |
| 2083 | *index = i; |
| 2084 | *disposition = NEW_FOREGROUND_TAB; |
| 2085 | return; |
| 2086 | } |
| 2087 | |
| 2088 | // Drop on tab |i|? |
| 2089 | if (point.x <= (frame.origin.x + |
| 2090 | (1.0 - kLRProportion) * frame.size.width)) { |
| 2091 | *index = i; |
| 2092 | *disposition = CURRENT_TAB; |
| 2093 | return; |
| 2094 | } |
| 2095 | |
| 2096 | // (Dropping in a new tab to the right of tab |i| will be taken care of in |
| 2097 | // the next iteration.) |
| 2098 | i++; |
| 2099 | } |
| 2100 | |
| 2101 | // If we've made it here, we want to append a new tab to the end. |
| 2102 | *index = -1; |
| 2103 | *disposition = NEW_FOREGROUND_TAB; |
| 2104 | } |
| 2105 | |
| 2106 | - (void)openURL:(GURL*)url inView:(NSView*)view at:(NSPoint)point { |
| 2107 | // Get the index and disposition. |
| 2108 | NSInteger index; |
| 2109 | WindowOpenDisposition disposition; |
| 2110 | [self droppingURLsAt:point |
| 2111 | givesIndex:&index |
| 2112 | disposition:&disposition]; |
| 2113 | |
| 2114 | // Either insert a new tab or open in a current tab. |
| 2115 | switch (disposition) { |
| 2116 | case NEW_FOREGROUND_TAB: { |
| 2117 | content::RecordAction(UserMetricsAction("Tab_DropURLBetweenTabs")); |
| 2118 | chrome::NavigateParams params(browser_, *url, |
| 2119 | content::PAGE_TRANSITION_TYPED); |
| 2120 | params.disposition = disposition; |
| 2121 | params.tabstrip_index = index; |
| 2122 | params.tabstrip_add_types = |
| 2123 | TabStripModel::ADD_ACTIVE | TabStripModel::ADD_FORCE_INDEX; |
| 2124 | chrome::Navigate(¶ms); |
| 2125 | break; |
| 2126 | } |
| 2127 | case CURRENT_TAB: { |
| 2128 | content::RecordAction(UserMetricsAction("Tab_DropURLOnTab")); |
| 2129 | OpenURLParams params( |
| 2130 | *url, Referrer(), CURRENT_TAB, content::PAGE_TRANSITION_TYPED, false); |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 2131 | tabStripModel_->GetWebContentsAt(index)->OpenURL(params); |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 2132 | tabStripModel_->ActivateTabAt(index, true); |
| 2133 | break; |
| 2134 | } |
| 2135 | default: |
| 2136 | NOTIMPLEMENTED(); |
| 2137 | } |
| 2138 | } |
| 2139 | |
| 2140 | // (URLDropTargetController protocol) |
| 2141 | - (void)dropURLs:(NSArray*)urls inView:(NSView*)view at:(NSPoint)point { |
| 2142 | DCHECK_EQ(view, tabStripView_.get()); |
| 2143 | |
| 2144 | if ([urls count] < 1) { |
| 2145 | NOTREACHED(); |
| 2146 | return; |
| 2147 | } |
| 2148 | |
| 2149 | //TODO(viettrungluu): dropping multiple URLs. |
| 2150 | if ([urls count] > 1) |
| 2151 | NOTIMPLEMENTED(); |
| 2152 | |
| 2153 | // Get the first URL and fix it up. |
| 2154 | GURL url(GURL(URLFixerUpper::FixupURL( |
| 2155 | base::SysNSStringToUTF8([urls objectAtIndex:0]), std::string()))); |
| 2156 | |
| 2157 | [self openURL:&url inView:view at:point]; |
| 2158 | } |
| 2159 | |
| 2160 | // (URLDropTargetController protocol) |
| 2161 | - (void)dropText:(NSString*)text inView:(NSView*)view at:(NSPoint)point { |
| 2162 | DCHECK_EQ(view, tabStripView_.get()); |
| 2163 | |
| 2164 | // If the input is plain text, classify the input and make the URL. |
| 2165 | AutocompleteMatch match; |
| 2166 | AutocompleteClassifierFactory::GetForProfile(browser_->profile())->Classify( |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 2167 | base::SysNSStringToUTF16(text), false, false, &match, NULL); |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 2168 | GURL url(match.destination_url); |
| 2169 | |
| 2170 | [self openURL:&url inView:view at:point]; |
| 2171 | } |
| 2172 | |
| 2173 | // (URLDropTargetController protocol) |
| 2174 | - (void)indicateDropURLsInView:(NSView*)view at:(NSPoint)point { |
| 2175 | DCHECK_EQ(view, tabStripView_.get()); |
| 2176 | |
| 2177 | // The minimum y-coordinate at which one should consider place the arrow. |
| 2178 | const CGFloat arrowBaseY = 25; |
| 2179 | |
| 2180 | NSInteger index; |
| 2181 | WindowOpenDisposition disposition; |
| 2182 | [self droppingURLsAt:point |
| 2183 | givesIndex:&index |
| 2184 | disposition:&disposition]; |
| 2185 | |
| 2186 | NSPoint arrowPos = NSMakePoint(0, arrowBaseY); |
| 2187 | if (index == -1) { |
| 2188 | // Append a tab at the end. |
| 2189 | DCHECK(disposition == NEW_FOREGROUND_TAB); |
| 2190 | NSInteger lastIndex = [tabArray_ count] - 1; |
| 2191 | NSRect overRect = [[[tabArray_ objectAtIndex:lastIndex] view] frame]; |
| 2192 | arrowPos.x = overRect.origin.x + overRect.size.width - kTabOverlap / 2.0; |
| 2193 | } else { |
| 2194 | NSRect overRect = [[[tabArray_ objectAtIndex:index] view] frame]; |
| 2195 | switch (disposition) { |
| 2196 | case NEW_FOREGROUND_TAB: |
| 2197 | // Insert tab (to the left of the given tab). |
| 2198 | arrowPos.x = overRect.origin.x + kTabOverlap / 2.0; |
| 2199 | break; |
| 2200 | case CURRENT_TAB: |
| 2201 | // Overwrite the given tab. |
| 2202 | arrowPos.x = overRect.origin.x + overRect.size.width / 2.0; |
| 2203 | break; |
| 2204 | default: |
| 2205 | NOTREACHED(); |
| 2206 | } |
| 2207 | } |
| 2208 | |
| 2209 | [tabStripView_ setDropArrowPosition:arrowPos]; |
| 2210 | [tabStripView_ setDropArrowShown:YES]; |
| 2211 | [tabStripView_ setNeedsDisplay:YES]; |
| 2212 | |
| 2213 | // Perform a delayed tab transition if hovering directly over a tab. |
| 2214 | if (index != -1 && disposition == CURRENT_TAB) { |
| 2215 | NSInteger modelIndex = [self modelIndexFromIndex:index]; |
| 2216 | // Only start the transition if it has a valid model index (i.e. it's not |
| 2217 | // in the middle of closing). |
| 2218 | if (modelIndex != NSNotFound) { |
| 2219 | hoverTabSelector_->StartTabTransition(modelIndex); |
| 2220 | return; |
| 2221 | } |
| 2222 | } |
| 2223 | // If a tab transition was not started, cancel the pending one. |
| 2224 | hoverTabSelector_->CancelTabTransition(); |
| 2225 | } |
| 2226 | |
| 2227 | // (URLDropTargetController protocol) |
| 2228 | - (void)hideDropURLsIndicatorInView:(NSView*)view { |
| 2229 | DCHECK_EQ(view, tabStripView_.get()); |
| 2230 | |
| 2231 | // Cancel any pending tab transition. |
| 2232 | hoverTabSelector_->CancelTabTransition(); |
| 2233 | |
| 2234 | if ([tabStripView_ dropArrowShown]) { |
| 2235 | [tabStripView_ setDropArrowShown:NO]; |
| 2236 | [tabStripView_ setNeedsDisplay:YES]; |
| 2237 | } |
| 2238 | } |
| 2239 | |
| 2240 | // (URLDropTargetController protocol) |
| 2241 | - (BOOL)isUnsupportedDropData:(id<NSDraggingInfo>)info { |
| 2242 | return drag_util::IsUnsupportedDropData(browser_->profile(), info); |
| 2243 | } |
| 2244 | |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 2245 | - (TabContentsController*)activeTabContentsController { |
| 2246 | int modelIndex = tabStripModel_->active_index(); |
| 2247 | if (modelIndex < 0) |
| 2248 | return nil; |
| 2249 | NSInteger index = [self indexFromModelIndex:modelIndex]; |
| 2250 | if (index < 0 || |
| 2251 | index >= (NSInteger)[tabContentsArray_ count]) |
| 2252 | return nil; |
| 2253 | return [tabContentsArray_ objectAtIndex:index]; |
| 2254 | } |
| 2255 | |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 2256 | - (void)themeDidChangeNotification:(NSNotification*)notification { |
| 2257 | [self setNewTabImages]; |
| 2258 | } |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 2259 | |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 2260 | - (void)setNewTabImages { |
| 2261 | ThemeService *theme = |
| 2262 | static_cast<ThemeService*>([[tabStripView_ window] themeProvider]); |
| 2263 | if (!theme) |
| 2264 | return; |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 2265 | |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 2266 | ResourceBundle& rb = ResourceBundle::GetSharedInstance(); |
| 2267 | NSImage* mask = rb.GetNativeImageNamed(IDR_NEWTAB_BUTTON_MASK).ToNSImage(); |
| 2268 | NSImage* normal = rb.GetNativeImageNamed(IDR_NEWTAB_BUTTON).ToNSImage(); |
| 2269 | NSImage* hover = rb.GetNativeImageNamed(IDR_NEWTAB_BUTTON_H).ToNSImage(); |
| 2270 | NSImage* pressed = rb.GetNativeImageNamed(IDR_NEWTAB_BUTTON_P).ToNSImage(); |
| 2271 | |
| 2272 | NSImage* foreground = ApplyMask( |
Ben Murdoch | bb1529c | 2013-08-08 10:24:53 +0100 | [diff] [blame^] | 2273 | theme->GetNSImageNamed(IDR_THEME_TAB_BACKGROUND), mask); |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 2274 | |
| 2275 | [[newTabButton_ cell] setImage:Overlay(foreground, normal, 1.0) |
| 2276 | forButtonState:image_button_cell::kDefaultState]; |
| 2277 | [[newTabButton_ cell] setImage:Overlay(foreground, hover, 1.0) |
| 2278 | forButtonState:image_button_cell::kHoverState]; |
| 2279 | [[newTabButton_ cell] setImage:Overlay(foreground, pressed, 1.0) |
| 2280 | forButtonState:image_button_cell::kPressedState]; |
| 2281 | |
| 2282 | // IDR_THEME_TAB_BACKGROUND_INACTIVE is only used with the default theme. |
| 2283 | if (theme->UsingDefaultTheme()) { |
| 2284 | const CGFloat alpha = tabs::kImageNoFocusAlpha; |
| 2285 | NSImage* background = ApplyMask( |
Ben Murdoch | bb1529c | 2013-08-08 10:24:53 +0100 | [diff] [blame^] | 2286 | theme->GetNSImageNamed(IDR_THEME_TAB_BACKGROUND_INACTIVE), mask); |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 2287 | [[newTabButton_ cell] setImage:Overlay(background, normal, alpha) |
| 2288 | forButtonState:image_button_cell::kDefaultStateBackground]; |
| 2289 | [[newTabButton_ cell] setImage:Overlay(background, hover, alpha) |
| 2290 | forButtonState:image_button_cell::kHoverStateBackground]; |
| 2291 | } else { |
| 2292 | [[newTabButton_ cell] setImage:nil |
| 2293 | forButtonState:image_button_cell::kDefaultStateBackground]; |
| 2294 | [[newTabButton_ cell] setImage:nil |
| 2295 | forButtonState:image_button_cell::kHoverStateBackground]; |
| 2296 | } |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 2297 | } |
| 2298 | |
| 2299 | @end |
| 2300 | |
| 2301 | NSView* GetSheetParentViewForWebContents(WebContents* web_contents) { |
| 2302 | // View hierarchy of the contents view: |
| 2303 | // NSView -- switchView, same for all tabs |
| 2304 | // +- NSView -- TabContentsController's view |
| 2305 | // +- TabContentsViewCocoa |
| 2306 | // |
| 2307 | // Changing it? Do not forget to modify |
| 2308 | // -[TabStripController swapInTabAtIndex:] too. |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 2309 | return [web_contents->GetView()->GetNativeView() superview]; |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 2310 | } |