OLD | NEW |
| (Empty) |
1 // Copyright 2014 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/profiles/avatar_menu_bubble_controller.h" | |
6 | |
7 #include "base/mac/bundle_locations.h" | |
8 #include "base/mac/sdk_forward_declarations.h" | |
9 #include "base/strings/sys_string_conversions.h" | |
10 #include "chrome/browser/browser_process.h" | |
11 #include "chrome/browser/profiles/avatar_menu.h" | |
12 #include "chrome/browser/profiles/profile_manager.h" | |
13 #include "chrome/browser/profiles/profile_metrics.h" | |
14 #include "chrome/browser/ui/browser.h" | |
15 #include "chrome/browser/ui/browser_window.h" | |
16 #import "chrome/browser/ui/cocoa/info_bubble_view.h" | |
17 #import "chrome/browser/ui/cocoa/info_bubble_window.h" | |
18 #include "chrome/grit/generated_resources.h" | |
19 #include "chrome/grit/theme_resources.h" | |
20 #import "third_party/google_toolbox_for_mac/src/AppKit/GTMUILocalizerAndLayoutTw
eaker.h" | |
21 #import "ui/base/cocoa/cocoa_base_utils.h" | |
22 #import "ui/base/cocoa/controls/hyperlink_button_cell.h" | |
23 #include "ui/base/l10n/l10n_util_mac.h" | |
24 #include "ui/base/resource/resource_bundle.h" | |
25 #include "ui/gfx/image/image.h" | |
26 | |
27 @interface AvatarMenuBubbleController (Private) | |
28 - (AvatarMenu*)menu; | |
29 - (NSView*)configureSupervisedUserInformation:(CGFloat)width; | |
30 - (NSButton*)configureNewUserButton:(CGFloat)yOffset | |
31 updateWidthAdjust:(CGFloat*)widthAdjust; | |
32 - (NSButton*)configureSwitchUserButton:(CGFloat)yOffset | |
33 updateWidthAdjust:(CGFloat*)widthAdjust; | |
34 - (AvatarMenuItemController*)initAvatarItem:(int)itemIndex | |
35 updateWidthAdjust:(CGFloat*)widthAdjust | |
36 setYOffset:(CGFloat)yOffset; | |
37 - (void)setWindowFrame:(CGFloat)yOffset widthAdjust:(CGFloat)width; | |
38 - (void)initMenuContents; | |
39 - (void)initSupervisedUserContents; | |
40 - (void)keyDown:(NSEvent*)theEvent; | |
41 - (void)moveDown:(id)sender; | |
42 - (void)moveUp:(id)sender; | |
43 - (void)insertNewline:(id)sender; | |
44 - (void)highlightNextItemByDelta:(NSInteger)delta; | |
45 - (void)highlightItem:(AvatarMenuItemController*)newItem; | |
46 @end | |
47 | |
48 namespace { | |
49 | |
50 // Constants taken from the Windows/Views implementation at: | |
51 // chrome/browser/ui/views/avatar_menu_bubble_view.cc | |
52 const CGFloat kBubbleMinWidth = 175; | |
53 const CGFloat kBubbleMaxWidth = 800; | |
54 const CGFloat kMaxItemTextWidth = 200; | |
55 | |
56 // Values derived from the XIB. | |
57 const CGFloat kVerticalSpacing = 10.0; | |
58 const CGFloat kLinkSpacing = 15.0; | |
59 const CGFloat kLabelInset = 49.0; | |
60 | |
61 // The offset of the supervised user information label and the "switch user" | |
62 // link. | |
63 const CGFloat kSupervisedUserSpacing = 26.0; | |
64 | |
65 } // namespace | |
66 | |
67 @implementation AvatarMenuBubbleController | |
68 | |
69 - (id)initWithBrowser:(Browser*)parentBrowser | |
70 anchoredAt:(NSPoint)point { | |
71 | |
72 // Pass in a NULL observer. Rebuilding while the bubble is open will cause it | |
73 // to be positioned incorrectly. Since the bubble will be dismissed on losing | |
74 // key status, it's impossible for the user to edit the information in a | |
75 // meaningful way such that it would need to be redrawn. | |
76 AvatarMenu* menu = new AvatarMenu( | |
77 &g_browser_process->profile_manager()->GetProfileAttributesStorage(), | |
78 NULL, parentBrowser); | |
79 menu->RebuildMenu(); | |
80 | |
81 if ((self = [self initWithMenu:menu | |
82 parentWindow:parentBrowser->window()->GetNativeWindow() | |
83 anchoredAt:point])) { | |
84 } | |
85 return self; | |
86 } | |
87 | |
88 - (IBAction)newProfile:(id)sender { | |
89 menu_->AddNewProfile(ProfileMetrics::ADD_NEW_USER_ICON); | |
90 } | |
91 | |
92 - (IBAction)switchToProfile:(id)sender { | |
93 // Check the event flags to see if a new window should be crated. | |
94 bool always_create = | |
95 ui::WindowOpenDispositionFromNSEvent([NSApp currentEvent]) == | |
96 WindowOpenDisposition::NEW_WINDOW; | |
97 menu_->SwitchToProfile([sender menuIndex], always_create, | |
98 ProfileMetrics::SWITCH_PROFILE_ICON); | |
99 } | |
100 | |
101 - (IBAction)editProfile:(id)sender { | |
102 menu_->EditProfile([sender menuIndex]); | |
103 } | |
104 | |
105 - (IBAction)switchProfile:(id)sender { | |
106 expanded_ = YES; | |
107 [self performLayout]; | |
108 } | |
109 | |
110 // Private ///////////////////////////////////////////////////////////////////// | |
111 | |
112 - (id)initWithMenu:(AvatarMenu*)menu | |
113 parentWindow:(NSWindow*)parent | |
114 anchoredAt:(NSPoint)point { | |
115 // Use an arbitrary height because it will reflect the size of the content. | |
116 NSRect contentRect = NSMakeRect(0, 0, kBubbleMinWidth, 150); | |
117 // Create an empty window into which content is placed. | |
118 base::scoped_nsobject<InfoBubbleWindow> window( | |
119 [[InfoBubbleWindow alloc] initWithContentRect:contentRect | |
120 styleMask:NSBorderlessWindowMask | |
121 backing:NSBackingStoreBuffered | |
122 defer:NO]); | |
123 if ((self = [super initWithWindow:window | |
124 parentWindow:parent | |
125 anchoredAt:point])) { | |
126 menu_.reset(menu); | |
127 | |
128 [window accessibilitySetOverrideValue: | |
129 l10n_util::GetNSString(IDS_PROFILES_BUBBLE_ACCESSIBLE_NAME) | |
130 forAttribute:NSAccessibilityTitleAttribute]; | |
131 [window accessibilitySetOverrideValue: | |
132 l10n_util::GetNSString(IDS_PROFILES_BUBBLE_ACCESSIBLE_DESCRIPTION) | |
133 forAttribute:NSAccessibilityHelpAttribute]; | |
134 | |
135 [[self bubble] setArrowLocation:info_bubble::kTopRight]; | |
136 [self performLayout]; | |
137 } | |
138 return self; | |
139 } | |
140 | |
141 - (AvatarMenuItemController*)initAvatarItem:(int)itemIndex | |
142 updateWidthAdjust:(CGFloat*)widthAdjust | |
143 setYOffset:(CGFloat)yOffset { | |
144 ResourceBundle& rb = ResourceBundle::GetSharedInstance(); | |
145 const AvatarMenu::Item& item = menu_->GetItemAt(itemIndex); | |
146 // Create the item view controller. Autorelease it because it will be owned | |
147 // by the |items_| array. | |
148 AvatarMenuItemController* itemView = | |
149 [[[AvatarMenuItemController alloc] initWithMenuIndex:itemIndex | |
150 menuController:self] autorelease]; | |
151 itemView.iconView.image = item.icon.ToNSImage(); | |
152 | |
153 // Adjust the name field to fit the string. If it overflows, record by how | |
154 // much the window needs to grow to accomodate the new size of the field. | |
155 NSTextField* nameField = itemView.nameField; | |
156 nameField.stringValue = base::SysUTF16ToNSString(item.name); | |
157 NSSize delta = [GTMUILocalizerAndLayoutTweaker sizeToFitView:nameField]; | |
158 if (NSWidth([nameField frame]) > kMaxItemTextWidth) { | |
159 delta.width -= (NSWidth([nameField frame]) - kMaxItemTextWidth); | |
160 NSRect frame = [nameField frame]; | |
161 frame.size.width = kMaxItemTextWidth; | |
162 [nameField setFrame:frame]; | |
163 if ([nameField respondsToSelector:@selector(setAllowsExpansionToolTips:)]) | |
164 [nameField setAllowsExpansionToolTips:YES]; | |
165 } | |
166 *widthAdjust = std::max(*widthAdjust, delta.width); | |
167 | |
168 // Repeat for the sync state/email. | |
169 NSTextField* emailField = itemView.emailField; | |
170 emailField.stringValue = base::SysUTF16ToNSString(item.username); | |
171 delta = [GTMUILocalizerAndLayoutTweaker sizeToFitView:emailField]; | |
172 if (NSWidth([emailField frame]) > kMaxItemTextWidth) { | |
173 delta.width -= (NSWidth([emailField frame]) - kMaxItemTextWidth); | |
174 NSRect frame = [emailField frame]; | |
175 frame.size.width = kMaxItemTextWidth; | |
176 [emailField setFrame:frame]; | |
177 } | |
178 *widthAdjust = std::max(*widthAdjust, delta.width); | |
179 | |
180 if (!item.active) { | |
181 // In the inactive case, hide additional UI. | |
182 [itemView.activeView setHidden:YES]; | |
183 [itemView.editButton setHidden:YES]; | |
184 } else { | |
185 // Otherwise, set up the edit button and its three interaction states. | |
186 itemView.activeView.image = | |
187 rb.GetImageNamed(IDR_PROFILE_SELECTED).ToNSImage(); | |
188 } | |
189 | |
190 // Add the item to the content view. | |
191 [[itemView view] setFrameOrigin:NSMakePoint(0, yOffset)]; | |
192 | |
193 // Keep track of the view controller. | |
194 [items_ addObject:itemView]; | |
195 return itemView; | |
196 } | |
197 | |
198 - (void)setWindowFrame:(CGFloat)yOffset widthAdjust:(CGFloat)width { | |
199 // Set the window frame, clamping the width at a sensible max. | |
200 NSRect frame = [[self window] frame]; | |
201 // Adjust the origin after we have switched from the supervised user menu to | |
202 // the regular menu. | |
203 CGFloat newWidth = std::min(kBubbleMinWidth + width, kBubbleMaxWidth); | |
204 if (expanded_) { | |
205 frame.origin.x += frame.size.width - newWidth; | |
206 frame.origin.y += frame.size.height - yOffset; | |
207 } | |
208 frame.size.height = yOffset; | |
209 frame.size.width = newWidth; | |
210 [[self window] setFrame:frame display:YES]; | |
211 } | |
212 | |
213 - (void)initMenuContents { | |
214 NSView* contentView = [[self window] contentView]; | |
215 | |
216 // |yOffset| is the next position at which to draw in contentView coordinates. | |
217 // Use a little more vertical spacing because the items have padding built- | |
218 // into the xib, and this gives a little more space to visually match. | |
219 CGFloat yOffset = kLinkSpacing; | |
220 CGFloat widthAdjust = 0; | |
221 | |
222 if (menu_->ShouldShowAddNewProfileLink()) { | |
223 // Since drawing happens bottom-up, start with the "New User" link. | |
224 NSButton* newButton = | |
225 [self configureNewUserButton:yOffset updateWidthAdjust:&widthAdjust]; | |
226 [contentView addSubview:newButton]; | |
227 yOffset += NSHeight([newButton frame]) + kVerticalSpacing; | |
228 | |
229 NSBox* separator = [self horizontalSeparatorWithFrame: | |
230 NSMakeRect(10, yOffset, NSWidth([contentView frame]) - 20, 0)]; | |
231 [separator setAutoresizingMask:NSViewWidthSizable]; | |
232 [contentView addSubview:separator]; | |
233 | |
234 yOffset += NSHeight([separator frame]); | |
235 } else { | |
236 yOffset = 7; | |
237 } | |
238 | |
239 // Loop over the profiles in reverse, constructing the menu items. | |
240 for (int i = menu_->GetNumberOfItems() - 1; i >= 0; --i) { | |
241 AvatarMenuItemController* itemView = [self initAvatarItem:i | |
242 updateWidthAdjust:&widthAdjust | |
243 setYOffset:yOffset]; | |
244 [contentView addSubview:[itemView view]]; | |
245 yOffset += NSHeight([[itemView view] frame]); | |
246 } | |
247 | |
248 yOffset += kVerticalSpacing * 1.5; | |
249 [self setWindowFrame:yOffset widthAdjust:widthAdjust]; | |
250 } | |
251 | |
252 - (void)initSupervisedUserContents { | |
253 NSView* contentView = [[self window] contentView]; | |
254 | |
255 // |yOffset| is the next position at which to draw in contentView coordinates. | |
256 // Use a little more vertical spacing because the items have padding built- | |
257 // into the xib, and this gives a little more space to visually match. | |
258 CGFloat yOffset = kLinkSpacing; | |
259 CGFloat widthAdjust = 0; | |
260 | |
261 // Since drawing happens bottom-up, start with the "Switch User" link. | |
262 NSButton* newButton = | |
263 [self configureSwitchUserButton:yOffset updateWidthAdjust:&widthAdjust]; | |
264 [contentView addSubview:newButton]; | |
265 yOffset += NSHeight([newButton frame]) + kVerticalSpacing; | |
266 | |
267 NSBox* separator = [self horizontalSeparatorWithFrame: | |
268 NSMakeRect(10, yOffset, NSWidth([contentView frame]) - 20, 0)]; | |
269 [separator setAutoresizingMask:NSViewWidthSizable]; | |
270 [contentView addSubview:separator]; | |
271 | |
272 yOffset += NSHeight([separator frame]) + kVerticalSpacing; | |
273 | |
274 // First init the active profile in order to determine the required width. We | |
275 // will have to adjust its frame later after adding general information about | |
276 // supervised users. | |
277 AvatarMenuItemController* itemView = | |
278 [self initAvatarItem:menu_->GetActiveProfileIndex() | |
279 updateWidthAdjust:&widthAdjust | |
280 setYOffset:yOffset]; | |
281 | |
282 // Don't increase the width too much (the total size should be at most | |
283 // |kBubbleMaxWidth|). | |
284 widthAdjust = std::min(widthAdjust, kBubbleMaxWidth - kBubbleMinWidth); | |
285 CGFloat newWidth = kBubbleMinWidth + widthAdjust; | |
286 | |
287 // Add general information about supervised users. | |
288 NSView* info = [self configureSupervisedUserInformation:newWidth]; | |
289 [info setFrameOrigin:NSMakePoint(0, yOffset)]; | |
290 [contentView addSubview:info]; | |
291 yOffset += NSHeight([info frame]) + kVerticalSpacing; | |
292 | |
293 separator = [self horizontalSeparatorWithFrame: | |
294 NSMakeRect(10, yOffset, NSWidth([contentView frame]) - 20, 0)]; | |
295 [separator setAutoresizingMask:NSViewWidthSizable]; | |
296 [contentView addSubview:separator]; | |
297 | |
298 yOffset += NSHeight([separator frame]); | |
299 | |
300 // Now update the frame of the active profile and add it. | |
301 NSRect frame = [[itemView view] frame]; | |
302 frame.origin.y = yOffset; | |
303 [[itemView view] setFrame:frame]; | |
304 [contentView addSubview:[itemView view]]; | |
305 | |
306 yOffset += NSHeight(frame) + kVerticalSpacing * 1.5; | |
307 [self setWindowFrame:yOffset widthAdjust:widthAdjust]; | |
308 } | |
309 | |
310 - (void)performLayout { | |
311 NSView* contentView = [[self window] contentView]; | |
312 | |
313 // Reset the array of controllers and remove all the views. | |
314 items_.reset([[NSMutableArray alloc] init]); | |
315 [contentView setSubviews:[NSArray array]]; | |
316 | |
317 if (menu_->GetSupervisedUserInformation().empty() || expanded_) | |
318 [self initMenuContents]; | |
319 else | |
320 [self initSupervisedUserContents]; | |
321 } | |
322 | |
323 - (NSView*)configureSupervisedUserInformation:(CGFloat)width { | |
324 base::scoped_nsobject<NSView> container( | |
325 [[NSView alloc] initWithFrame:NSZeroRect]); | |
326 | |
327 // Add the limited user icon on the left side of the information TextView. | |
328 base::scoped_nsobject<NSImageView> iconView( | |
329 [[NSImageView alloc] initWithFrame:NSMakeRect(5, 0, 16, 16)]); | |
330 [container addSubview:iconView]; | |
331 | |
332 NSString* info = | |
333 base::SysUTF16ToNSString(menu_->GetSupervisedUserInformation()); | |
334 NSDictionary* attributes = | |
335 @{ NSFontAttributeName : [NSFont labelFontOfSize:12] }; | |
336 base::scoped_nsobject<NSAttributedString> attrString( | |
337 [[NSAttributedString alloc] initWithString:info attributes:attributes]); | |
338 base::scoped_nsobject<NSTextView> label( | |
339 [[NSTextView alloc] initWithFrame:NSMakeRect( | |
340 kSupervisedUserSpacing, 0, width - kSupervisedUserSpacing - 5, 0)]); | |
341 [[label textStorage] setAttributedString:attrString]; | |
342 [label setHorizontallyResizable:NO]; | |
343 [label setEditable:NO]; | |
344 [label sizeToFit]; | |
345 [container addSubview:label]; | |
346 [container setFrameSize:NSMakeSize(width, NSHeight([label frame]))]; | |
347 | |
348 // Reposition the limited user icon so that it is on top. | |
349 [iconView setFrameOrigin:NSMakePoint(5, NSHeight([label frame]) - 16)]; | |
350 return container.autorelease(); | |
351 } | |
352 | |
353 - (NSButton*)configureNewUserButton:(CGFloat)yOffset | |
354 updateWidthAdjust:(CGFloat*)widthAdjust { | |
355 base::scoped_nsobject<NSButton> newButton([[NSButton alloc] initWithFrame: | |
356 NSMakeRect(kLabelInset, yOffset, kBubbleMinWidth - kLabelInset, 16)]); | |
357 base::scoped_nsobject<HyperlinkButtonCell> buttonCell( | |
358 [[HyperlinkButtonCell alloc] initTextCell: | |
359 l10n_util::GetNSString(IDS_PROFILES_CREATE_NEW_PROFILE_LINK)]); | |
360 [newButton setCell:buttonCell.get()]; | |
361 [newButton setFont:[NSFont labelFontOfSize:12.0]]; | |
362 [newButton setBezelStyle:NSRegularSquareBezelStyle]; | |
363 [newButton setTarget:self]; | |
364 [newButton setAction:@selector(newProfile:)]; | |
365 NSSize delta = [GTMUILocalizerAndLayoutTweaker sizeToFitView:newButton]; | |
366 if (delta.width > 0) | |
367 *widthAdjust = std::max(*widthAdjust, delta.width); | |
368 return newButton.autorelease(); | |
369 } | |
370 | |
371 - (NSButton*)configureSwitchUserButton:(CGFloat)yOffset | |
372 updateWidthAdjust:(CGFloat*)widthAdjust { | |
373 base::scoped_nsobject<NSButton> newButton( | |
374 [[NSButton alloc] initWithFrame:NSMakeRect( | |
375 kSupervisedUserSpacing, yOffset, kBubbleMinWidth - kLabelInset, 16)]); | |
376 base::scoped_nsobject<HyperlinkButtonCell> buttonCell( | |
377 [[HyperlinkButtonCell alloc] initTextCell: | |
378 l10n_util::GetNSString(IDS_PROFILES_SWITCH_PROFILE_LINK)]); | |
379 [newButton setCell:buttonCell.get()]; | |
380 [newButton setFont:[NSFont labelFontOfSize:12.0]]; | |
381 [newButton setBezelStyle:NSRegularSquareBezelStyle]; | |
382 [newButton setTarget:self]; | |
383 [newButton setAction:@selector(switchProfile:)]; | |
384 NSSize delta = [GTMUILocalizerAndLayoutTweaker sizeToFitView:newButton]; | |
385 if (delta.width > 0) | |
386 *widthAdjust = std::max(*widthAdjust, delta.width); | |
387 return newButton.autorelease(); | |
388 } | |
389 | |
390 - (NSMutableArray*)items { | |
391 return items_.get(); | |
392 } | |
393 | |
394 - (void)keyDown:(NSEvent*)theEvent { | |
395 [self interpretKeyEvents:[NSArray arrayWithObject:theEvent]]; | |
396 } | |
397 | |
398 - (void)moveDown:(id)sender { | |
399 [self highlightNextItemByDelta:-1]; | |
400 } | |
401 | |
402 - (void)moveUp:(id)sender { | |
403 [self highlightNextItemByDelta:1]; | |
404 } | |
405 | |
406 - (void)insertNewline:(id)sender { | |
407 for (AvatarMenuItemController* item in items_.get()) { | |
408 if ([item isHighlighted]) { | |
409 [self switchToProfile:item]; | |
410 return; | |
411 } | |
412 } | |
413 } | |
414 | |
415 - (void)highlightNextItemByDelta:(NSInteger)delta { | |
416 NSUInteger count = [items_ count]; | |
417 if (count == 0) | |
418 return; | |
419 | |
420 NSInteger old_index = -1; | |
421 for (NSUInteger i = 0; i < count; ++i) { | |
422 if ([[items_ objectAtIndex:i] isHighlighted]) { | |
423 old_index = i; | |
424 break; | |
425 } | |
426 } | |
427 | |
428 NSInteger new_index; | |
429 // If nothing is selected then start at the top if we're going down and start | |
430 // at the bottom if we're going up. | |
431 if (old_index == -1) | |
432 new_index = delta < 0 ? (count - 1) : 0; | |
433 else | |
434 new_index = old_index + delta; | |
435 | |
436 // Cap the index. We don't wrap around to match the behavior of Mac menus. | |
437 new_index = | |
438 std::min(std::max(static_cast<NSInteger>(0), new_index), | |
439 static_cast<NSInteger>(count - 1)); | |
440 | |
441 [self highlightItem:[items_ objectAtIndex:new_index]]; | |
442 } | |
443 | |
444 - (void)highlightItem:(AvatarMenuItemController*)newItem { | |
445 AvatarMenuItemController* oldItem = nil; | |
446 for (AvatarMenuItemController* item in items_.get()) { | |
447 if ([item isHighlighted]) { | |
448 oldItem = item; | |
449 break; | |
450 } | |
451 } | |
452 | |
453 if (oldItem == newItem) | |
454 return; | |
455 | |
456 [oldItem setIsHighlighted:NO]; | |
457 [newItem setIsHighlighted:YES]; | |
458 } | |
459 | |
460 | |
461 @end | |
462 | |
463 // Menu Item Controller //////////////////////////////////////////////////////// | |
464 | |
465 @interface AvatarMenuItemController (Private) | |
466 - (void)animateFromView:(NSView*)outView toView:(NSView*)inView; | |
467 @end | |
468 | |
469 @implementation AvatarMenuItemController | |
470 | |
471 @synthesize menuIndex = menuIndex_; | |
472 @synthesize isHighlighted = isHighlighted_; | |
473 @synthesize iconView = iconView_; | |
474 @synthesize activeView = activeView_; | |
475 @synthesize nameField = nameField_; | |
476 @synthesize emailField = emailField_; | |
477 @synthesize editButton = editButton_; | |
478 | |
479 - (id)initWithMenuIndex:(size_t)menuIndex | |
480 menuController:(AvatarMenuBubbleController*)controller { | |
481 if ((self = [super initWithNibName:@"AvatarMenuItem" | |
482 bundle:base::mac::FrameworkBundle()])) { | |
483 propertyReleaser_.Init(self, [AvatarMenuItemController class]); | |
484 menuIndex_ = menuIndex; | |
485 controller_ = controller; | |
486 [self loadView]; | |
487 [nameField_ setAutoresizingMask:NSViewNotSizable]; | |
488 [[nameField_ cell] setLineBreakMode:NSLineBreakByTruncatingTail]; | |
489 [emailField_ setAutoresizingMask:NSViewNotSizable]; | |
490 [[emailField_ cell] setLineBreakMode:NSLineBreakByTruncatingTail]; | |
491 } | |
492 return self; | |
493 } | |
494 | |
495 - (void)dealloc { | |
496 static_cast<AvatarMenuItemView*>(self.view).viewController = nil; | |
497 [linkAnimation_ stopAnimation]; | |
498 [linkAnimation_ setDelegate:nil]; | |
499 [super dealloc]; | |
500 } | |
501 | |
502 - (void)awakeFromNib { | |
503 [GTMUILocalizerAndLayoutTweaker sizeToFitView:self.editButton]; | |
504 self.editButton.hidden = YES; | |
505 } | |
506 | |
507 - (IBAction)switchToProfile:(id)sender { | |
508 [controller_ switchToProfile:self]; | |
509 } | |
510 | |
511 - (IBAction)editProfile:(id)sender { | |
512 [controller_ editProfile:self]; | |
513 } | |
514 | |
515 - (void)highlightForEventType:(NSEventType)type { | |
516 switch (type) { | |
517 case NSMouseEntered: | |
518 [controller_ highlightItem:self]; | |
519 break; | |
520 | |
521 case NSMouseExited: | |
522 [controller_ highlightItem:nil]; | |
523 break; | |
524 | |
525 default: | |
526 NOTREACHED(); | |
527 }; | |
528 } | |
529 | |
530 - (void)setIsHighlighted:(BOOL)isHighlighted { | |
531 if (isHighlighted_ == isHighlighted) | |
532 return; | |
533 | |
534 isHighlighted_ = isHighlighted; | |
535 [[self view] setNeedsDisplay:YES]; | |
536 | |
537 // Cancel any running animation. | |
538 if (linkAnimation_.get()) { | |
539 [NSObject cancelPreviousPerformRequestsWithTarget:linkAnimation_ | |
540 selector:@selector(startAnimation) | |
541 object:nil]; | |
542 } | |
543 | |
544 // Fade the edit link in or out only if this is the active view. | |
545 if (self.activeView.isHidden) | |
546 return; | |
547 | |
548 if (isHighlighted_) { | |
549 [self animateFromView:self.emailField toView:self.editButton]; | |
550 } else { | |
551 // If the edit button is visible or the animation to make it so is | |
552 // running, stop the animation and fade it back to the email. If not, then | |
553 // don't run an animation to prevent flickering. | |
554 if (!self.editButton.isHidden || [linkAnimation_ isAnimating]) { | |
555 [linkAnimation_ stopAnimation]; | |
556 linkAnimation_.reset(); | |
557 [self animateFromView:self.editButton toView:self.emailField]; | |
558 } | |
559 } | |
560 } | |
561 | |
562 - (void)animateFromView:(NSView*)outView toView:(NSView*)inView { | |
563 const NSTimeInterval kAnimationDuration = 0.175; | |
564 | |
565 NSDictionary* outDict = [NSDictionary dictionaryWithObjectsAndKeys: | |
566 outView, NSViewAnimationTargetKey, | |
567 NSViewAnimationFadeOutEffect, NSViewAnimationEffectKey, | |
568 nil | |
569 ]; | |
570 NSDictionary* inDict = [NSDictionary dictionaryWithObjectsAndKeys: | |
571 inView, NSViewAnimationTargetKey, | |
572 NSViewAnimationFadeInEffect, NSViewAnimationEffectKey, | |
573 nil | |
574 ]; | |
575 | |
576 linkAnimation_.reset([[NSViewAnimation alloc] initWithViewAnimations: | |
577 [NSArray arrayWithObjects:outDict, inDict, nil]]); | |
578 [linkAnimation_ setDelegate:self]; | |
579 [linkAnimation_ setDuration:kAnimationDuration]; | |
580 | |
581 [self willStartAnimation:linkAnimation_]; | |
582 | |
583 [linkAnimation_ performSelector:@selector(startAnimation) | |
584 withObject:nil | |
585 afterDelay:0.2]; | |
586 } | |
587 | |
588 - (void)willStartAnimation:(NSAnimation*)animation { | |
589 } | |
590 | |
591 - (void)animationDidEnd:(NSAnimation*)animation { | |
592 if (animation == linkAnimation_.get()) | |
593 linkAnimation_.reset(); | |
594 } | |
595 | |
596 - (void)animationDidStop:(NSAnimation*)animation { | |
597 if (animation == linkAnimation_.get()) | |
598 linkAnimation_.reset(); | |
599 } | |
600 | |
601 @end | |
602 | |
603 // Profile Switch Button /////////////////////////////////////////////////////// | |
604 | |
605 @implementation AvatarMenuItemView | |
606 | |
607 @synthesize viewController = viewController_; | |
608 | |
609 - (void)awakeFromNib { | |
610 [self updateTrackingAreas]; | |
611 } | |
612 | |
613 - (void)updateTrackingAreas { | |
614 if (trackingArea_.get()) | |
615 [self removeTrackingArea:trackingArea_.get()]; | |
616 | |
617 trackingArea_.reset( | |
618 [[CrTrackingArea alloc] initWithRect:[self bounds] | |
619 options:NSTrackingMouseEnteredAndExited | | |
620 NSTrackingActiveInKeyWindow | |
621 owner:self | |
622 userInfo:nil]); | |
623 [self addTrackingArea:trackingArea_.get()]; | |
624 | |
625 [super updateTrackingAreas]; | |
626 } | |
627 | |
628 - (void)mouseEntered:(id)sender { | |
629 [viewController_ highlightForEventType:[[NSApp currentEvent] type]]; | |
630 [self setNeedsDisplay:YES]; | |
631 } | |
632 | |
633 - (void)mouseExited:(id)sender { | |
634 [viewController_ highlightForEventType:[[NSApp currentEvent] type]]; | |
635 [self setNeedsDisplay:YES]; | |
636 } | |
637 | |
638 - (void)mouseUp:(id)sender { | |
639 [viewController_ switchToProfile:self]; | |
640 } | |
641 | |
642 - (void)drawRect:(NSRect)dirtyRect { | |
643 NSColor* backgroundColor = nil; | |
644 if ([viewController_ isHighlighted]) { | |
645 backgroundColor = [NSColor colorWithCalibratedRed:223.0/255 | |
646 green:238.0/255 | |
647 blue:246.0/255 | |
648 alpha:1.0]; | |
649 } else { | |
650 backgroundColor = [NSColor clearColor]; | |
651 } | |
652 | |
653 [backgroundColor set]; | |
654 [NSBezierPath fillRect:[self bounds]]; | |
655 } | |
656 | |
657 // Make sure the element is focusable for accessibility. | |
658 - (BOOL)canBecomeKeyView { | |
659 return YES; | |
660 } | |
661 | |
662 - (BOOL)accessibilityIsIgnored { | |
663 return NO; | |
664 } | |
665 | |
666 - (NSArray*)accessibilityAttributeNames { | |
667 NSMutableArray* attributes = | |
668 [[super accessibilityAttributeNames] mutableCopy]; | |
669 [attributes addObject:NSAccessibilityTitleAttribute]; | |
670 [attributes addObject:NSAccessibilityEnabledAttribute]; | |
671 | |
672 return [attributes autorelease]; | |
673 } | |
674 | |
675 - (NSArray*)accessibilityActionNames { | |
676 NSArray* parentActions = [super accessibilityActionNames]; | |
677 return [parentActions arrayByAddingObject:NSAccessibilityPressAction]; | |
678 } | |
679 | |
680 - (id)accessibilityAttributeValue:(NSString*)attribute { | |
681 if ([attribute isEqual:NSAccessibilityRoleAttribute]) | |
682 return NSAccessibilityButtonRole; | |
683 | |
684 if ([attribute isEqual:NSAccessibilityRoleDescriptionAttribute]) | |
685 return NSAccessibilityRoleDescription(NSAccessibilityButtonRole, nil); | |
686 | |
687 if ([attribute isEqual:NSAccessibilityTitleAttribute]) { | |
688 return l10n_util::GetNSStringF( | |
689 IDS_PROFILES_SWITCH_TO_PROFILE_ACCESSIBLE_NAME, | |
690 base::SysNSStringToUTF16(self.viewController.nameField.stringValue)); | |
691 } | |
692 | |
693 if ([attribute isEqual:NSAccessibilityEnabledAttribute]) | |
694 return [NSNumber numberWithBool:YES]; | |
695 | |
696 return [super accessibilityAttributeValue:attribute]; | |
697 } | |
698 | |
699 - (void)accessibilityPerformAction:(NSString*)action { | |
700 if ([action isEqual:NSAccessibilityPressAction]) { | |
701 [viewController_ switchToProfile:self]; | |
702 return; | |
703 } | |
704 | |
705 [super accessibilityPerformAction:action]; | |
706 } | |
707 | |
708 @end | |
709 | |
710 //////////////////////////////////////////////////////////////////////////////// | |
711 | |
712 @implementation AccessibilityIgnoredImageCell | |
713 - (BOOL)accessibilityIsIgnored { | |
714 return YES; | |
715 } | |
716 @end | |
717 | |
718 @implementation AccessibilityIgnoredTextFieldCell | |
719 - (BOOL)accessibilityIsIgnored { | |
720 return YES; | |
721 } | |
722 @end | |
OLD | NEW |