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/location_bar/autocomplete_text_field_cell.h" 6 7 #include "base/logging.h" 8 #include "base/mac/foundation_util.h" 9 #include "base/mac/mac_logging.h" 10 #include "chrome/browser/search/search.h" 11 #import "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field.h" 12 #import "chrome/browser/ui/cocoa/location_bar/button_decoration.h" 13 #import "chrome/browser/ui/cocoa/location_bar/location_bar_decoration.h" 14 #import "extensions/common/feature_switch.h" 15 #include "grit/theme_resources.h" 16 #import "third_party/mozilla/NSPasteboard+Utils.h" 17 #import "ui/base/cocoa/appkit_utils.h" 18 #import "ui/base/cocoa/nsview_additions.h" 19 #import "ui/base/cocoa/tracking_area.h" 20 #include "ui/gfx/scoped_ns_graphics_context_save_gstate_mac.h" 21 22 using extensions::FeatureSwitch; 23 24 namespace { 25 26 // Matches the clipping radius of |GradientButtonCell|. 27 const CGFloat kCornerRadius = 3.0; 28 29 // How far to inset the left- and right-hand decorations from the field's 30 // bounds. 31 const CGFloat kLeftDecorationXOffset = 5.0; 32 const CGFloat kRightDecorationXOffset = 5.0; 33 34 // The amount of padding on either side reserved for drawing 35 // decorations. [Views has |kItemPadding| == 3.] 36 const CGFloat kDecorationHorizontalPad = 3.0; 37 38 NSString* const kButtonDecorationKey = @"ButtonDecoration"; 39 40 const ui::NinePartImageIds kPopupBorderImageIds = 41 IMAGE_GRID(IDR_OMNIBOX_POPUP_BORDER_AND_SHADOW); 42 43 const ui::NinePartImageIds kNormalBorderImageIds = IMAGE_GRID(IDR_TEXTFIELD); 44 45 // How long to wait for mouse-up on the location icon before assuming 46 // that the user wants to drag. 47 const NSTimeInterval kLocationIconDragTimeout = 0.25; 48 49 // Calculate the positions for a set of decorations. |frame| is the 50 // overall frame to do layout in, |remaining_frame| will get the 51 // left-over space. |all_decorations| is the set of decorations to 52 // lay out, |decorations| will be set to the decorations which are 53 // visible and which fit, in the same order as |all_decorations|, 54 // while |decoration_frames| will be the corresponding frames. 55 // |x_edge| describes the edge to layout the decorations against 56 // (|NSMinXEdge| or |NSMaxXEdge|). |regular_padding| is the padding 57 // from the edge of |cell_frame| to use when the first visible decoration 58 // is a regular decoration. |action_padding| is the padding to use when the 59 // first decoration is a button decoration, ie. the action box button. 60 // (|kDecorationHorizontalPad| is used between decorations). 61 void CalculatePositionsHelper( 62 NSRect frame, 63 const std::vector<LocationBarDecoration*>& all_decorations, 64 NSRectEdge x_edge, 65 CGFloat regular_padding, 66 CGFloat action_padding, 67 std::vector<LocationBarDecoration*>* decorations, 68 std::vector<NSRect>* decoration_frames, 69 NSRect* remaining_frame) { 70 DCHECK(x_edge == NSMinXEdge || x_edge == NSMaxXEdge); 71 DCHECK_EQ(decorations->size(), decoration_frames->size()); 72 73 // The initial padding depends on whether the first visible decoration is 74 // a button or not. 75 bool is_first_visible_decoration = true; 76 77 for (size_t i = 0; i < all_decorations.size(); ++i) { 78 if (all_decorations[i]->IsVisible()) { 79 CGFloat padding = kDecorationHorizontalPad; 80 if (is_first_visible_decoration) { 81 padding = all_decorations[i]->AsButtonDecoration() ? 82 action_padding : regular_padding; 83 is_first_visible_decoration = false; 84 } 85 86 NSRect padding_rect, available; 87 88 // Peel off the outside padding. 89 NSDivideRect(frame, &padding_rect, &available, padding, x_edge); 90 91 // Find out how large the decoration will be in the remaining 92 // space. 93 const CGFloat used_width = 94 all_decorations[i]->GetWidthForSpace(NSWidth(available)); 95 96 if (used_width != LocationBarDecoration::kOmittedWidth) { 97 DCHECK_GT(used_width, 0.0); 98 NSRect decoration_frame; 99 100 // Peel off the desired width, leaving the remainder in 101 // |frame|. 102 NSDivideRect(available, &decoration_frame, &frame, 103 used_width, x_edge); 104 105 decorations->push_back(all_decorations[i]); 106 decoration_frames->push_back(decoration_frame); 107 DCHECK_EQ(decorations->size(), decoration_frames->size()); 108 109 // Adjust padding for between decorations. 110 padding = kDecorationHorizontalPad; 111 } 112 } 113 } 114 115 DCHECK_EQ(decorations->size(), decoration_frames->size()); 116 *remaining_frame = frame; 117 } 118 119 // Helper function for calculating placement of decorations w/in the cell. 120 // |frame| is the cell's boundary rectangle, |remaining_frame| will get any 121 // space left after decorations are laid out (for text). |left_decorations| is 122 // a set of decorations for the left-hand side of the cell, |right_decorations| 123 // for the right-hand side. |edge_width| is the width of one vertical edge of 124 // the omnibox, this depends on whether the display is low DPI or high DPI. 125 // |decorations| will contain the resulting visible decorations, and 126 // |decoration_frames| will contain their frames in the same coordinates as 127 // |frame|. Decorations will be ordered left to right. As a convenience returns 128 // the index of the first right-hand decoration. 129 size_t CalculatePositionsInFrame( 130 NSRect frame, 131 const std::vector<LocationBarDecoration*>& left_decorations, 132 const std::vector<LocationBarDecoration*>& right_decorations, 133 CGFloat edge_width, 134 std::vector<LocationBarDecoration*>* decorations, 135 std::vector<NSRect>* decoration_frames, 136 NSRect* remaining_frame) { 137 decorations->clear(); 138 decoration_frames->clear(); 139 140 // Layout |left_decorations| against the LHS. 141 CalculatePositionsHelper(frame, left_decorations, NSMinXEdge, 142 kLeftDecorationXOffset, edge_width, 143 decorations, decoration_frames, &frame); 144 DCHECK_EQ(decorations->size(), decoration_frames->size()); 145 146 // Capture the number of visible left-hand decorations. 147 const size_t left_count = decorations->size(); 148 149 // Layout |right_decorations| against the RHS. 150 CalculatePositionsHelper(frame, right_decorations, NSMaxXEdge, 151 kRightDecorationXOffset, edge_width, decorations, 152 decoration_frames, &frame); 153 DCHECK_EQ(decorations->size(), decoration_frames->size()); 154 155 // Reverse the right-hand decorations so that overall everything is 156 // sorted left to right. 157 std::reverse(decorations->begin() + left_count, decorations->end()); 158 std::reverse(decoration_frames->begin() + left_count, 159 decoration_frames->end()); 160 161 *remaining_frame = frame; 162 return left_count; 163 } 164 165 } // namespace 166 167 @interface AutocompleteTextFieldCell () 168 // Post an OnSetFocus notification to the observer of |controlView|. 169 - (void)focusNotificationFor:(NSEvent*)event 170 ofView:(AutocompleteTextField*)controlView; 171 @end 172 173 @implementation AutocompleteTextFieldCell 174 175 @synthesize isPopupMode = isPopupMode_; 176 177 - (CGFloat)topTextFrameOffset { 178 return 3.0; 179 } 180 181 - (CGFloat)bottomTextFrameOffset { 182 return 3.0; 183 } 184 185 - (CGFloat)cornerRadius { 186 return kCornerRadius; 187 } 188 189 - (CGFloat)edgeWidth { 190 // The omnibox vertical edge width is 1 pixel both in low DPI and high DPI. 191 return [[self controlView] cr_lineWidth]; 192 } 193 194 - (BOOL)shouldDrawBezel { 195 return YES; 196 } 197 198 - (CGFloat)lineHeight { 199 return 17; 200 } 201 202 - (void)clearDecorations { 203 leftDecorations_.clear(); 204 rightDecorations_.clear(); 205 } 206 207 - (void)addLeftDecoration:(LocationBarDecoration*)decoration { 208 leftDecorations_.push_back(decoration); 209 } 210 211 - (void)addRightDecoration:(LocationBarDecoration*)decoration { 212 rightDecorations_.push_back(decoration); 213 } 214 215 - (CGFloat)availableWidthInFrame:(const NSRect)frame { 216 std::vector<LocationBarDecoration*> decorations; 217 std::vector<NSRect> decorationFrames; 218 NSRect textFrame; 219 CalculatePositionsInFrame(frame, leftDecorations_, rightDecorations_, 220 [self edgeWidth], &decorations, &decorationFrames, 221 &textFrame); 222 223 return NSWidth(textFrame); 224 } 225 226 - (NSRect)frameForDecoration:(const LocationBarDecoration*)aDecoration 227 inFrame:(NSRect)cellFrame { 228 // Short-circuit if the decoration is known to be not visible. 229 if (aDecoration && !aDecoration->IsVisible()) 230 return NSZeroRect; 231 232 // Layout the decorations. 233 std::vector<LocationBarDecoration*> decorations; 234 std::vector<NSRect> decorationFrames; 235 NSRect textFrame; 236 CalculatePositionsInFrame(cellFrame, leftDecorations_, rightDecorations_, 237 [self edgeWidth], &decorations, &decorationFrames, 238 &textFrame); 239 240 // Find our decoration and return the corresponding frame. 241 std::vector<LocationBarDecoration*>::const_iterator iter = 242 std::find(decorations.begin(), decorations.end(), aDecoration); 243 if (iter != decorations.end()) { 244 const size_t index = iter - decorations.begin(); 245 return decorationFrames[index]; 246 } 247 248 // Decorations which are not visible should have been filtered out 249 // at the top, but return |NSZeroRect| rather than a 0-width rect 250 // for consistency. 251 NOTREACHED(); 252 return NSZeroRect; 253 } 254 255 // Overriden to account for the decorations. 256 - (NSRect)textFrameForFrame:(NSRect)cellFrame { 257 // Get the frame adjusted for decorations. 258 std::vector<LocationBarDecoration*> decorations; 259 std::vector<NSRect> decorationFrames; 260 NSRect textFrame = [super textFrameForFrame:cellFrame]; 261 CalculatePositionsInFrame(textFrame, leftDecorations_, rightDecorations_, 262 [self edgeWidth], &decorations, &decorationFrames, 263 &textFrame); 264 265 // NOTE: This function must closely match the logic in 266 // |-drawInteriorWithFrame:inView:|. 267 268 return textFrame; 269 } 270 271 // Returns the sub-frame where clicks can happen within the cell. 272 - (NSRect)clickableFrameForFrame:(NSRect)cellFrame { 273 return [super textFrameForFrame:cellFrame]; 274 } 275 276 - (NSRect)textCursorFrameForFrame:(NSRect)cellFrame { 277 std::vector<LocationBarDecoration*> decorations; 278 std::vector<NSRect> decorationFrames; 279 NSRect textFrame; 280 size_t left_count = 281 CalculatePositionsInFrame(cellFrame, leftDecorations_, rightDecorations_, 282 [self edgeWidth], &decorations, 283 &decorationFrames, &textFrame); 284 285 // Determine the left-most extent for the i-beam cursor. 286 CGFloat minX = NSMinX(textFrame); 287 for (size_t index = left_count; index--; ) { 288 if (decorations[index]->AcceptsMousePress()) 289 break; 290 291 // If at leftmost decoration, expand to edge of cell. 292 if (!index) { 293 minX = NSMinX(cellFrame); 294 } else { 295 minX = NSMinX(decorationFrames[index]) - kDecorationHorizontalPad; 296 } 297 } 298 299 // Determine the right-most extent for the i-beam cursor. 300 CGFloat maxX = NSMaxX(textFrame); 301 for (size_t index = left_count; index < decorations.size(); ++index) { 302 if (decorations[index]->AcceptsMousePress()) 303 break; 304 305 // If at rightmost decoration, expand to edge of cell. 306 if (index == decorations.size() - 1) { 307 maxX = NSMaxX(cellFrame); 308 } else { 309 maxX = NSMaxX(decorationFrames[index]) + kDecorationHorizontalPad; 310 } 311 } 312 313 // I-beam cursor covers left-most to right-most. 314 return NSMakeRect(minX, NSMinY(textFrame), maxX - minX, NSHeight(textFrame)); 315 } 316 317 - (void)drawWithFrame:(NSRect)frame inView:(NSView*)controlView { 318 // Background color. 319 const CGFloat lineWidth = [controlView cr_lineWidth]; 320 if (isPopupMode_) { 321 [[self backgroundColor] set]; 322 NSRectFillUsingOperation(NSInsetRect(frame, 1, 1), NSCompositeSourceOver); 323 } else { 324 CGFloat insetSize = lineWidth == 0.5 ? 1.5 : 2.0; 325 NSRect fillRect = NSInsetRect(frame, insetSize, insetSize); 326 [[self backgroundColor] set]; 327 [[NSBezierPath bezierPathWithRoundedRect:fillRect 328 xRadius:kCornerRadius 329 yRadius:kCornerRadius] fill]; 330 } 331 332 // Border. 333 ui::DrawNinePartImage(frame, 334 isPopupMode_ ? kPopupBorderImageIds 335 : kNormalBorderImageIds, 336 NSCompositeSourceOver, 337 1.0, 338 true); 339 340 // Interior contents. Drawn after the border as some of the interior controls 341 // draw over the border. 342 [self drawInteriorWithFrame:frame inView:controlView]; 343 344 // Focus ring. 345 if ([self showsFirstResponder]) { 346 NSRect focusRingRect = NSInsetRect(frame, lineWidth, lineWidth); 347 [[[NSColor keyboardFocusIndicatorColor] 348 colorWithAlphaComponent:0.5 / lineWidth] set]; 349 NSBezierPath* path = [NSBezierPath bezierPathWithRoundedRect:focusRingRect 350 xRadius:kCornerRadius 351 yRadius:kCornerRadius]; 352 [path setLineWidth:lineWidth * 2.0]; 353 [path stroke]; 354 } 355 } 356 357 - (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView*)controlView { 358 std::vector<LocationBarDecoration*> decorations; 359 std::vector<NSRect> decorationFrames; 360 NSRect workingFrame; 361 362 CalculatePositionsInFrame(cellFrame, leftDecorations_, rightDecorations_, 363 [self edgeWidth], &decorations, &decorationFrames, 364 &workingFrame); 365 366 // Draw the decorations. 367 for (size_t i = 0; i < decorations.size(); ++i) { 368 if (decorations[i]) { 369 NSRect background_frame = NSInsetRect( 370 decorationFrames[i], -(kDecorationHorizontalPad + 1) / 2, 2); 371 decorations[i]->DrawWithBackgroundInFrame( 372 background_frame, decorationFrames[i], controlView); 373 } 374 } 375 376 // NOTE: This function must closely match the logic in 377 // |-textFrameForFrame:|. 378 379 // Superclass draws text portion WRT original |cellFrame|. 380 // Even though -isOpaque is NO due to rounded corners, we know that the text 381 // will be drawn on top of an opaque area, therefore it is safe to enable 382 // font smoothing. 383 { 384 gfx::ScopedNSGraphicsContextSaveGState scopedGState; 385 NSGraphicsContext* context = [NSGraphicsContext currentContext]; 386 CGContextRef cgContext = static_cast<CGContextRef>([context graphicsPort]); 387 CGContextSetShouldSmoothFonts(cgContext, true); 388 [super drawInteriorWithFrame:cellFrame inView:controlView]; 389 } 390 } 391 392 - (LocationBarDecoration*)decorationForEvent:(NSEvent*)theEvent 393 inRect:(NSRect)cellFrame 394 ofView:(AutocompleteTextField*)controlView 395 { 396 const BOOL flipped = [controlView isFlipped]; 397 const NSPoint location = 398 [controlView convertPoint:[theEvent locationInWindow] fromView:nil]; 399 400 std::vector<LocationBarDecoration*> decorations; 401 std::vector<NSRect> decorationFrames; 402 NSRect textFrame; 403 CalculatePositionsInFrame(cellFrame, leftDecorations_, rightDecorations_, 404 [self edgeWidth], &decorations, &decorationFrames, 405 &textFrame); 406 407 for (size_t i = 0; i < decorations.size(); ++i) { 408 if (NSMouseInRect(location, decorationFrames[i], flipped)) 409 return decorations[i]; 410 } 411 412 return NULL; 413 } 414 415 - (NSMenu*)decorationMenuForEvent:(NSEvent*)theEvent 416 inRect:(NSRect)cellFrame 417 ofView:(AutocompleteTextField*)controlView { 418 LocationBarDecoration* decoration = 419 [self decorationForEvent:theEvent inRect:cellFrame ofView:controlView]; 420 if (decoration) 421 return decoration->GetMenu(); 422 return nil; 423 } 424 425 - (BOOL)mouseDown:(NSEvent*)theEvent 426 inRect:(NSRect)cellFrame 427 ofView:(AutocompleteTextField*)controlView { 428 // TODO(groby): Factor this into three pieces - find target for event, handle 429 // delayed focus (for any and all events), execute mouseDown for target. 430 431 // Check if this mouseDown was the reason the control became firstResponder. 432 // If not, discard focus event. 433 base::scoped_nsobject<NSEvent> focusEvent(focusEvent_.release()); 434 if (![theEvent isEqual:focusEvent]) 435 focusEvent.reset(); 436 437 LocationBarDecoration* decoration = 438 [self decorationForEvent:theEvent inRect:cellFrame ofView:controlView]; 439 if (!decoration || !decoration->AcceptsMousePress()) 440 return NO; 441 442 NSRect decorationRect = 443 [self frameForDecoration:decoration inFrame:cellFrame]; 444 445 // If the decoration is draggable, then initiate a drag if the user 446 // drags or holds the mouse down for awhile. 447 if (decoration->IsDraggable()) { 448 NSDate* timeout = 449 [NSDate dateWithTimeIntervalSinceNow:kLocationIconDragTimeout]; 450 NSEvent* event = [NSApp nextEventMatchingMask:(NSLeftMouseDraggedMask | 451 NSLeftMouseUpMask) 452 untilDate:timeout 453 inMode:NSEventTrackingRunLoopMode 454 dequeue:YES]; 455 if (!event || [event type] == NSLeftMouseDragged) { 456 NSPasteboard* pboard = decoration->GetDragPasteboard(); 457 DCHECK(pboard); 458 459 NSImage* image = decoration->GetDragImage(); 460 DCHECK(image); 461 462 NSRect dragImageRect = decoration->GetDragImageFrame(decorationRect); 463 464 // Center under mouse horizontally, with cursor below so the image 465 // can be seen. 466 const NSPoint mousePoint = 467 [controlView convertPoint:[theEvent locationInWindow] fromView:nil]; 468 dragImageRect.origin = 469 NSMakePoint(mousePoint.x - NSWidth(dragImageRect) / 2.0, 470 mousePoint.y - NSHeight(dragImageRect)); 471 472 // -[NSView dragImage:at:*] wants the images lower-left point, 473 // regardless of -isFlipped. Converting the rect to window base 474 // coordinates doesn't require any special-casing. Note that 475 // -[NSView dragFile:fromRect:*] takes a rect rather than a 476 // point, likely for this exact reason. 477 const NSPoint dragPoint = 478 [controlView convertRect:dragImageRect toView:nil].origin; 479 [[controlView window] dragImage:image 480 at:dragPoint 481 offset:NSZeroSize 482 event:theEvent 483 pasteboard:pboard 484 source:self 485 slideBack:YES]; 486 487 return YES; 488 } 489 490 // On mouse-up fall through to mouse-pressed case. 491 DCHECK_EQ([event type], NSLeftMouseUp); 492 } 493 494 bool handled; 495 if (decoration->AsButtonDecoration()) { 496 ButtonDecoration* button = decoration->AsButtonDecoration(); 497 498 button->SetButtonState(ButtonDecoration::kButtonStatePressed); 499 [controlView setNeedsDisplay:YES]; 500 501 // Track the mouse until the user releases the button. 502 [self trackMouse:theEvent 503 inRect:decorationRect 504 ofView:controlView 505 untilMouseUp:YES]; 506 507 const NSPoint mouseLocation = [[NSApp currentEvent] locationInWindow]; 508 const NSPoint point = [controlView convertPoint:mouseLocation fromView:nil]; 509 510 // Post delayed focus notification, if necessary. 511 if (focusEvent.get() && !button->PreventFocus(point)) 512 [self focusNotificationFor:focusEvent ofView:controlView]; 513 focusEvent.reset(); 514 515 // Set the proper state (hover or normal) once the mouse has been released, 516 // and call |OnMousePressed| if the button was released while the mouse was 517 // within the bounds of the button. 518 if (NSMouseInRect(point, decorationRect, [controlView isFlipped])) { 519 button->SetButtonState(ButtonDecoration::kButtonStateHover); 520 [controlView setNeedsDisplay:YES]; 521 handled = decoration->OnMousePressed( 522 [self frameForDecoration:decoration inFrame:cellFrame], 523 NSMakePoint(point.x - decorationRect.origin.x, 524 point.y - decorationRect.origin.y)); 525 } else { 526 button->SetButtonState(ButtonDecoration::kButtonStateNormal); 527 [controlView setNeedsDisplay:YES]; 528 handled = true; 529 } 530 } else { 531 const NSPoint mouseLocation = [theEvent locationInWindow]; 532 const NSPoint point = [controlView convertPoint:mouseLocation fromView:nil]; 533 handled = decoration->OnMousePressed( 534 decorationRect, 535 NSMakePoint(point.x - decorationRect.origin.x, 536 point.y - decorationRect.origin.y)); 537 } 538 539 return handled ? YES : NO; 540 } 541 542 // Helper method for the |mouseEntered:inView:| and |mouseExited:inView:| 543 // messages. Retrieves the |ButtonDecoration| for the specified event (received 544 // from a tracking area), and returns |NULL| if no decoration matches. 545 - (ButtonDecoration*)getButtonDecorationForEvent:(NSEvent*)theEvent { 546 ButtonDecoration* bd = static_cast<ButtonDecoration*>( 547 [[[[theEvent trackingArea] userInfo] valueForKey:kButtonDecorationKey] 548 pointerValue]); 549 550 CHECK(!bd || 551 std::count(leftDecorations_.begin(), leftDecorations_.end(), bd) || 552 std::count(rightDecorations_.begin(), rightDecorations_.end(), bd)); 553 554 return bd; 555 } 556 557 // Helper method for |setUpTrackingAreasInView|. Creates an |NSDictionary| to 558 // be used as user information to identify which decoration is the source of an 559 // event (from a tracking area). 560 - (NSDictionary*)getDictionaryForButtonDecoration: 561 (ButtonDecoration*)decoration { 562 if (!decoration) 563 return nil; 564 565 DCHECK( 566 std::count(leftDecorations_.begin(), leftDecorations_.end(), decoration) || 567 std::count(rightDecorations_.begin(), rightDecorations_.end(), decoration)); 568 569 return [NSDictionary 570 dictionaryWithObject:[NSValue valueWithPointer:decoration] 571 forKey:kButtonDecorationKey]; 572 } 573 574 - (void)mouseEntered:(NSEvent*)theEvent 575 inView:(AutocompleteTextField*)controlView { 576 ButtonDecoration* decoration = [self getButtonDecorationForEvent:theEvent]; 577 if (decoration) { 578 decoration->SetButtonState(ButtonDecoration::kButtonStateHover); 579 [controlView setNeedsDisplay:YES]; 580 } 581 } 582 583 - (void)mouseExited:(NSEvent*)theEvent 584 inView:(AutocompleteTextField*)controlView { 585 ButtonDecoration* decoration = [self getButtonDecorationForEvent:theEvent]; 586 if (decoration) { 587 decoration->SetButtonState(ButtonDecoration::kButtonStateNormal); 588 [controlView setNeedsDisplay:YES]; 589 } 590 } 591 592 - (void)setUpTrackingAreasInRect:(NSRect)frame 593 ofView:(AutocompleteTextField*)view { 594 std::vector<LocationBarDecoration*> decorations; 595 std::vector<NSRect> decorationFrames; 596 NSRect textFrame; 597 NSRect cellRect = [self clickableFrameForFrame:[view bounds]]; 598 CalculatePositionsInFrame(cellRect, leftDecorations_, rightDecorations_, 599 [self edgeWidth], &decorations, &decorationFrames, 600 &textFrame); 601 602 // Remove previously-registered tracking areas, since we'll update them below. 603 for (CrTrackingArea* area in [view trackingAreas]) { 604 if ([[area userInfo] objectForKey:kButtonDecorationKey]) 605 [view removeTrackingArea:area]; 606 } 607 608 // Setup new tracking areas for the buttons. 609 for (size_t i = 0; i < decorations.size(); ++i) { 610 ButtonDecoration* button = decorations[i]->AsButtonDecoration(); 611 if (button) { 612 // If the button isn't pressed (in which case we want to leave it as-is), 613 // update it's state since we might have missed some entered/exited events 614 // because of the removing/adding of the tracking areas. 615 if (button->GetButtonState() != 616 ButtonDecoration::kButtonStatePressed) { 617 const NSPoint mouseLocationWindow = 618 [[view window] mouseLocationOutsideOfEventStream]; 619 const NSPoint mouseLocation = 620 [view convertPoint:mouseLocationWindow fromView:nil]; 621 const BOOL mouseInRect = NSMouseInRect( 622 mouseLocation, decorationFrames[i], [view isFlipped]); 623 button->SetButtonState(mouseInRect ? 624 ButtonDecoration::kButtonStateHover : 625 ButtonDecoration::kButtonStateNormal); 626 [view setNeedsDisplay:YES]; 627 } 628 629 NSDictionary* info = [self getDictionaryForButtonDecoration:button]; 630 base::scoped_nsobject<CrTrackingArea> area( 631 [[CrTrackingArea alloc] initWithRect:decorationFrames[i] 632 options:NSTrackingMouseEnteredAndExited | 633 NSTrackingActiveAlways 634 owner:view 635 userInfo:info]); 636 [view addTrackingArea:area]; 637 } 638 } 639 } 640 641 // Given a newly created .webloc plist url file, also give it a resource 642 // fork and insert 'TEXT and 'url ' resources holding further copies of the 643 // url data. This is required for apps such as Terminal and Safari to accept it 644 // as a real webloc file when dragged in. 645 // It's expected that the resource fork requirement will go away at some 646 // point and this code can then be deleted. 647 OSErr WriteURLToNewWebLocFileResourceFork(NSURL* file, NSString* urlStr) { 648 ResFileRefNum refNum = kResFileNotOpened; 649 ResFileRefNum prevResRef = CurResFile(); 650 FSRef fsRef; 651 OSErr err = noErr; 652 HFSUniStr255 resourceForkName; 653 FSGetResourceForkName(&resourceForkName); 654 655 if (![[NSFileManager defaultManager] fileExistsAtPath:[file path]]) 656 return fnfErr; 657 658 if (!CFURLGetFSRef((CFURLRef)file, &fsRef)) 659 return fnfErr; 660 661 err = FSCreateResourceFork(&fsRef, 662 resourceForkName.length, 663 resourceForkName.unicode, 664 0); 665 if (err) 666 return err; 667 err = FSOpenResourceFile(&fsRef, 668 resourceForkName.length, 669 resourceForkName.unicode, 670 fsRdWrPerm, &refNum); 671 if (err) 672 return err; 673 674 const char* utf8URL = [urlStr UTF8String]; 675 int urlChars = strlen(utf8URL); 676 677 Handle urlHandle = NewHandle(urlChars); 678 memcpy(*urlHandle, utf8URL, urlChars); 679 680 Handle textHandle = NewHandle(urlChars); 681 memcpy(*textHandle, utf8URL, urlChars); 682 683 // Data for the 'drag' resource. 684 // This comes from derezzing webloc files made by the Finder. 685 // It's bigendian data, so it's represented here as chars to preserve 686 // byte order. 687 char dragData[] = { 688 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, // Header. 689 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 690 0x54, 0x45, 0x58, 0x54, 0x00, 0x00, 0x01, 0x00, // 'TEXT', 0, 256 691 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 692 0x75, 0x72, 0x6C, 0x20, 0x00, 0x00, 0x01, 0x00, // 'url ', 0, 256 693 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 694 }; 695 Handle dragHandle = NewHandleClear(sizeof(dragData)); 696 memcpy(*dragHandle, &dragData[0], sizeof(dragData)); 697 698 // Save the resources to the file. 699 ConstStr255Param noName = {0}; 700 AddResource(dragHandle, 'drag', 128, noName); 701 AddResource(textHandle, 'TEXT', 256, noName); 702 AddResource(urlHandle, 'url ', 256, noName); 703 704 CloseResFile(refNum); 705 UseResFile(prevResRef); 706 return noErr; 707 } 708 709 // Returns the file path for file |name| if saved at NSURL |base|. 710 static NSString* PathWithBaseURLAndName(NSURL* base, NSString* name) { 711 NSString* filteredName = 712 [name stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; 713 return [[NSURL URLWithString:filteredName relativeToURL:base] path]; 714 } 715 716 // Returns if there is already a file |name| at dir NSURL |base|. 717 static BOOL FileAlreadyExists(NSURL* base, NSString* name) { 718 NSString* path = PathWithBaseURLAndName(base, name); 719 DCHECK([path hasSuffix:name]); 720 return [[NSFileManager defaultManager] fileExistsAtPath:path]; 721 } 722 723 // Takes a destination URL, a suggested file name, & an extension (eg .webloc). 724 // Returns the complete file name with extension you should use. 725 // The name returned will not contain /, : or ?, will not be longer than 726 // kMaxNameLength + length of extension, and will not be a file name that 727 // already exists in that directory. If necessary it will try appending a space 728 // and a number to the name (but before the extension) trying numbers up to and 729 // including kMaxIndex. 730 // If the function gives up it returns nil. 731 static NSString* UnusedLegalNameForNewDropFile(NSURL* saveLocation, 732 NSString *fileName, 733 NSString *extension) { 734 int number = 1; 735 const int kMaxIndex = 20; 736 const unsigned kMaxNameLength = 64; // Arbitrary. 737 738 NSString* filteredName = [fileName stringByReplacingOccurrencesOfString:@"/" 739 withString:@"-"]; 740 filteredName = [filteredName stringByReplacingOccurrencesOfString:@":" 741 withString:@"-"]; 742 filteredName = [filteredName stringByReplacingOccurrencesOfString:@"?" 743 withString:@"-"]; 744 745 if ([filteredName length] > kMaxNameLength) 746 filteredName = [filteredName substringToIndex:kMaxNameLength]; 747 748 NSString* candidateName = [filteredName stringByAppendingString:extension]; 749 750 while (FileAlreadyExists(saveLocation, candidateName)) { 751 if (number > kMaxIndex) 752 return nil; 753 else 754 candidateName = [filteredName stringByAppendingFormat:@" %d%@", 755 number++, extension]; 756 } 757 758 return candidateName; 759 } 760 761 - (NSArray*)namesOfPromisedFilesDroppedAtDestination:(NSURL*)dropDestination { 762 NSPasteboard* pboard = [NSPasteboard pasteboardWithName:NSDragPboard]; 763 NSFileManager* fileManager = [NSFileManager defaultManager]; 764 765 if (![pboard containsURLData]) 766 return NULL; 767 768 NSArray *urls = NULL; 769 NSArray* titles = NULL; 770 [pboard getURLs:&urls andTitles:&titles convertingFilenames:YES]; 771 772 NSString* urlStr = [urls objectAtIndex:0]; 773 NSString* nameStr = [titles objectAtIndex:0]; 774 775 NSString* nameWithExtensionStr = 776 UnusedLegalNameForNewDropFile(dropDestination, nameStr, @".webloc"); 777 if (!nameWithExtensionStr) 778 return NULL; 779 780 NSDictionary* urlDict = [NSDictionary dictionaryWithObject:urlStr 781 forKey:@"URL"]; 782 NSURL* outputURL = 783 [NSURL fileURLWithPath:PathWithBaseURLAndName(dropDestination, 784 nameWithExtensionStr)]; 785 [urlDict writeToURL:outputURL 786 atomically:NO]; 787 788 if (![fileManager fileExistsAtPath:[outputURL path]]) 789 return NULL; 790 791 NSDictionary* attr = [NSDictionary dictionaryWithObjectsAndKeys: 792 [NSNumber numberWithBool:YES], NSFileExtensionHidden, 793 [NSNumber numberWithUnsignedLong:'ilht'], NSFileHFSTypeCode, 794 [NSNumber numberWithUnsignedLong:'MACS'], NSFileHFSCreatorCode, 795 nil]; 796 [fileManager setAttributes:attr 797 ofItemAtPath:[outputURL path] 798 error:nil]; 799 // Add resource data. 800 OSErr resStatus = WriteURLToNewWebLocFileResourceFork(outputURL, urlStr); 801 OSSTATUS_DCHECK(resStatus == noErr, resStatus); 802 803 return [NSArray arrayWithObject:nameWithExtensionStr]; 804 } 805 806 - (NSDragOperation)draggingSourceOperationMaskForLocal:(BOOL)isLocal { 807 return NSDragOperationCopy; 808 } 809 810 - (void)updateToolTipsInRect:(NSRect)cellFrame 811 ofView:(AutocompleteTextField*)controlView { 812 std::vector<LocationBarDecoration*> decorations; 813 std::vector<NSRect> decorationFrames; 814 NSRect textFrame; 815 CalculatePositionsInFrame(cellFrame, leftDecorations_, rightDecorations_, 816 [self edgeWidth], &decorations, &decorationFrames, 817 &textFrame); 818 819 for (size_t i = 0; i < decorations.size(); ++i) { 820 NSString* tooltip = decorations[i]->GetToolTip(); 821 if ([tooltip length] > 0) 822 [controlView addToolTip:tooltip forRect:decorationFrames[i]]; 823 } 824 } 825 826 - (BOOL)hideFocusState { 827 return hideFocusState_; 828 } 829 830 - (void)setHideFocusState:(BOOL)hideFocusState 831 ofView:(AutocompleteTextField*)controlView { 832 if (hideFocusState_ == hideFocusState) 833 return; 834 hideFocusState_ = hideFocusState; 835 [controlView setNeedsDisplay:YES]; 836 NSTextView* fieldEditor = 837 base::mac::ObjCCastStrict<NSTextView>([controlView currentEditor]); 838 [fieldEditor updateInsertionPointStateAndRestartTimer:YES]; 839 } 840 841 - (BOOL)showsFirstResponder { 842 return [super showsFirstResponder] && !hideFocusState_; 843 } 844 845 - (void)focusNotificationFor:(NSEvent*)event 846 ofView:(AutocompleteTextField*)controlView { 847 if ([controlView observer]) { 848 const bool controlDown = ([event modifierFlags] & NSControlKeyMask) != 0; 849 [controlView observer]->OnSetFocus(controlDown); 850 } 851 } 852 853 - (void)handleFocusEvent:(NSEvent*)event 854 ofView:(AutocompleteTextField*)controlView { 855 // Only intercept left button click. All other events cause immediate focus. 856 if ([event type] == NSLeftMouseDown) { 857 LocationBarDecoration* decoration = 858 [self decorationForEvent:event 859 inRect:[controlView bounds] 860 ofView:controlView]; 861 // Only ButtonDecorations need a delayed focus handling. 862 if (decoration && decoration->AsButtonDecoration()) { 863 focusEvent_.reset([event retain]); 864 return; 865 } 866 } 867 868 // Handle event immediately. 869 [self focusNotificationFor:event ofView:controlView]; 870 } 871 872 @end 873