| // Copyright (c) 2011 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_editor.h" |
| |
| #include "base/strings/string_util.h" |
| #include "base/strings/sys_string_conversions.h" |
| #include "chrome/app/chrome_command_ids.h" // IDC_* |
| #include "chrome/browser/ui/browser_list.h" |
| #import "chrome/browser/ui/cocoa/browser_window_controller.h" |
| #import "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field.h" |
| #import "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_cell.h" |
| #import "chrome/browser/ui/cocoa/toolbar/toolbar_controller.h" |
| #include "grit/generated_resources.h" |
| #import "ui/base/cocoa/find_pasteboard.h" |
| #include "ui/base/l10n/l10n_util_mac.h" |
| |
| namespace { |
| |
| // When too much data is put into a single-line text field, things get |
| // janky due to the cost of computing the blink rect. Sometimes users |
| // accidentally paste large amounts, so place a limit on what will be |
| // accepted. |
| // |
| // 10k characters was arbitrarily chosen by seeing how much a text |
| // field could handle in a single line before it started getting too |
| // janky to recover from (jankiness was detectable around 5k). |
| // www.google.com returns an error for searches around 2k characters, |
| // so this is conservative. |
| const NSUInteger kMaxPasteLength = 10000; |
| |
| // Returns |YES| if too much text would be pasted. |
| BOOL ThePasteboardIsTooDamnBig() { |
| NSPasteboard* pb = [NSPasteboard generalPasteboard]; |
| NSString* type = |
| [pb availableTypeFromArray:[NSArray arrayWithObject:NSStringPboardType]]; |
| if (!type) |
| return NO; |
| |
| return [[pb stringForType:type] length] > kMaxPasteLength; |
| } |
| |
| } // namespace |
| |
| @implementation AutocompleteTextFieldEditor |
| |
| - (BOOL)shouldDrawInsertionPoint { |
| return [super shouldDrawInsertionPoint] && |
| ![[[self delegate] cell] hideFocusState]; |
| } |
| |
| - (id)initWithFrame:(NSRect)frameRect { |
| if ((self = [super initWithFrame:frameRect])) { |
| dropHandler_.reset([[URLDropTargetHandler alloc] initWithView:self]); |
| |
| forbiddenCharacters_.reset([[NSCharacterSet controlCharacterSet] retain]); |
| |
| // These checks seem inappropriate to the omnibox, and also |
| // unlikely to work reliably due to our autocomplete interfering. |
| // |
| // Also see <http://crbug.com/173405>. |
| NSTextCheckingTypes checkingTypes = [self enabledTextCheckingTypes]; |
| checkingTypes &= ~NSTextCheckingTypeReplacement; |
| checkingTypes &= ~NSTextCheckingTypeCorrection; |
| [self setEnabledTextCheckingTypes:checkingTypes]; |
| } |
| return self; |
| } |
| |
| // If the entire field is selected, drag the same data as would be |
| // dragged from the field's location icon. In some cases the textual |
| // contents will not contain relevant data (for instance, "http://" is |
| // stripped from URLs). |
| - (BOOL)dragSelectionWithEvent:(NSEvent *)event |
| offset:(NSSize)mouseOffset |
| slideBack:(BOOL)slideBack { |
| AutocompleteTextFieldObserver* observer = [self observer]; |
| DCHECK(observer); |
| if (observer && observer->CanCopy()) { |
| NSPasteboard* pboard = [NSPasteboard pasteboardWithName:NSDragPboard]; |
| observer->CopyToPasteboard(pboard); |
| |
| NSPoint p; |
| NSImage* image = [self dragImageForSelectionWithEvent:event origin:&p]; |
| |
| [self dragImage:image |
| at:p |
| offset:mouseOffset |
| event:event |
| pasteboard:pboard |
| source:self |
| slideBack:slideBack]; |
| return YES; |
| } |
| return [super dragSelectionWithEvent:event |
| offset:mouseOffset |
| slideBack:slideBack]; |
| } |
| |
| - (void)copy:(id)sender { |
| AutocompleteTextFieldObserver* observer = [self observer]; |
| DCHECK(observer); |
| if (observer && observer->CanCopy()) |
| observer->CopyToPasteboard([NSPasteboard generalPasteboard]); |
| } |
| |
| - (void)cut:(id)sender { |
| [self copy:sender]; |
| [self delete:nil]; |
| } |
| |
| - (void)copyURL:(id)sender { |
| AutocompleteTextFieldObserver* observer = [self observer]; |
| DCHECK(observer); |
| if (observer->CanCopy()) |
| observer->CopyURLToPasteboard([NSPasteboard generalPasteboard]); |
| } |
| |
| // This class assumes that the delegate is an AutocompleteTextField. |
| // Enforce that assumption. |
| - (AutocompleteTextField*)delegate { |
| AutocompleteTextField* delegate = |
| static_cast<AutocompleteTextField*>([super delegate]); |
| DCHECK(delegate == nil || |
| [delegate isKindOfClass:[AutocompleteTextField class]]); |
| return delegate; |
| } |
| |
| - (void)setDelegate:(AutocompleteTextField*)delegate { |
| DCHECK(delegate == nil || |
| [delegate isKindOfClass:[AutocompleteTextField class]]); |
| |
| // Unregister from any previously registered undo and redo notifications. |
| NSNotificationCenter* nc = [NSNotificationCenter defaultCenter]; |
| [nc removeObserver:self |
| name:NSUndoManagerDidUndoChangeNotification |
| object:nil]; |
| [nc removeObserver:self |
| name:NSUndoManagerDidRedoChangeNotification |
| object:nil]; |
| |
| // Set the delegate. |
| [super setDelegate:delegate]; |
| |
| // Register for undo and redo notifications from the new |delegate|, if it is |
| // non-nil. |
| if ([self delegate]) { |
| NSUndoManager* undo_manager = [self undoManager]; |
| [nc addObserver:self |
| selector:@selector(didUndoOrRedo:) |
| name:NSUndoManagerDidUndoChangeNotification |
| object:undo_manager]; |
| [nc addObserver:self |
| selector:@selector(didUndoOrRedo:) |
| name:NSUndoManagerDidRedoChangeNotification |
| object:undo_manager]; |
| } |
| } |
| |
| - (void)didUndoOrRedo:(NSNotification *)aNotification { |
| AutocompleteTextFieldObserver* observer = [self observer]; |
| if (observer) |
| observer->OnDidChange(); |
| } |
| |
| // Convenience method for retrieving the observer from the delegate. |
| - (AutocompleteTextFieldObserver*)observer { |
| return [[self delegate] observer]; |
| } |
| |
| - (void)paste:(id)sender { |
| if (ThePasteboardIsTooDamnBig()) { |
| NSBeep(); |
| return; |
| } |
| |
| AutocompleteTextFieldObserver* observer = [self observer]; |
| DCHECK(observer); |
| if (observer) { |
| observer->OnPaste(); |
| } |
| } |
| |
| - (void)pasteAndMatchStyle:(id)sender { |
| [self paste:sender]; |
| } |
| |
| - (void)pasteAndGo:sender { |
| if (ThePasteboardIsTooDamnBig()) { |
| NSBeep(); |
| return; |
| } |
| |
| AutocompleteTextFieldObserver* observer = [self observer]; |
| DCHECK(observer); |
| if (observer) { |
| observer->OnPasteAndGo(); |
| } |
| } |
| |
| // We have rich text, but it shouldn't be modified by the user, so |
| // don't update the font panel. In theory, -setUsesFontPanel: should |
| // accomplish this, but that gets called frequently with YES when |
| // NSTextField and NSTextView synchronize their contents. That is |
| // probably unavoidable because in most cases having rich text in the |
| // field you probably would expect it to update the font panel. |
| - (void)updateFontPanel {} |
| |
| // No ruler bar, so don't update any of that state, either. |
| - (void)updateRuler {} |
| |
| - (NSMenu*)menuForEvent:(NSEvent*)event { |
| // Give the control a chance to provide page-action menus. |
| // NOTE: Note that page actions aren't even in the editor's |
| // boundaries! The Cocoa control implementation seems to do a |
| // blanket forward to here if nothing more specific is returned from |
| // the control and cell calls. |
| // TODO(shess): Determine if the page-action part of this can be |
| // moved to the cell. |
| NSMenu* actionMenu = [[self delegate] decorationMenuForEvent:event]; |
| if (actionMenu) |
| return actionMenu; |
| |
| NSMenu* menu = [[[NSMenu alloc] initWithTitle:@"TITLE"] autorelease]; |
| [menu addItemWithTitle:l10n_util::GetNSStringWithFixup(IDS_CUT) |
| action:@selector(cut:) |
| keyEquivalent:@""]; |
| [menu addItemWithTitle:l10n_util::GetNSStringWithFixup(IDS_COPY) |
| action:@selector(copy:) |
| keyEquivalent:@""]; |
| |
| if ([self isEditable]) { |
| // Copy URL if the URL has been replaced by the Extended Instant API. |
| DCHECK([self observer]); |
| NSString* label = l10n_util::GetNSStringWithFixup(IDS_COPY_URL_MAC); |
| [menu addItemWithTitle:label |
| action:@selector(copyURL:) |
| keyEquivalent:@""]; |
| } |
| |
| [menu addItemWithTitle:l10n_util::GetNSStringWithFixup(IDS_PASTE) |
| action:@selector(paste:) |
| keyEquivalent:@""]; |
| |
| // TODO(shess): If the control is not editable, should we show a |
| // greyed-out "Paste and Go"? |
| if ([self isEditable]) { |
| // Paste and go/search. |
| if (!ThePasteboardIsTooDamnBig()) { |
| AutocompleteTextFieldObserver* observer = [self observer]; |
| DCHECK(observer); |
| const int string_id = observer->GetPasteActionStringId(); |
| NSString* label = l10n_util::GetNSStringWithFixup(string_id); |
| DCHECK([label length]); |
| [menu addItemWithTitle:label |
| action:@selector(pasteAndGo:) |
| keyEquivalent:@""]; |
| } |
| |
| [menu addItem:[NSMenuItem separatorItem]]; |
| |
| NSString* search_engine_label = |
| l10n_util::GetNSStringWithFixup(IDS_EDIT_SEARCH_ENGINES); |
| DCHECK([search_engine_label length]); |
| NSMenuItem* item = [menu addItemWithTitle:search_engine_label |
| action:@selector(commandDispatch:) |
| keyEquivalent:@""]; |
| [item setTag:IDC_EDIT_SEARCH_ENGINES]; |
| } |
| |
| return menu; |
| } |
| |
| // (Overridden from NSResponder) |
| - (BOOL)becomeFirstResponder { |
| BOOL doAccept = [super becomeFirstResponder]; |
| AutocompleteTextField* field = [self delegate]; |
| // Only lock visibility if we've been set up with a delegate (the text field). |
| if (doAccept && field) { |
| // Give the text field ownership of the visibility lock. (The first |
| // responder dance between the field and the field editor is a little |
| // weird.) |
| [[BrowserWindowController browserWindowControllerForView:field] |
| lockBarVisibilityForOwner:field withAnimation:YES delay:NO]; |
| } |
| return doAccept; |
| } |
| |
| // (Overridden from NSResponder) |
| - (BOOL)resignFirstResponder { |
| BOOL doResign = [super resignFirstResponder]; |
| AutocompleteTextField* field = [self delegate]; |
| // Only lock visibility if we've been set up with a delegate (the text field). |
| if (doResign && field) { |
| // Give the text field ownership of the visibility lock. |
| [[BrowserWindowController browserWindowControllerForView:field] |
| releaseBarVisibilityForOwner:field withAnimation:YES delay:YES]; |
| |
| AutocompleteTextFieldObserver* observer = [self observer]; |
| if (observer) |
| observer->OnKillFocus(); |
| } |
| return doResign; |
| } |
| |
| - (void)mouseDown:(NSEvent*)event { |
| AutocompleteTextFieldObserver* observer = [self observer]; |
| if (observer) |
| observer->OnMouseDown([event buttonNumber]); |
| [super mouseDown:event]; |
| } |
| |
| - (void)rightMouseDown:(NSEvent *)event { |
| AutocompleteTextFieldObserver* observer = [self observer]; |
| if (observer) |
| observer->OnMouseDown([event buttonNumber]); |
| [super rightMouseDown:event]; |
| } |
| |
| - (void)otherMouseDown:(NSEvent *)event { |
| AutocompleteTextFieldObserver* observer = [self observer]; |
| if (observer) |
| observer->OnMouseDown([event buttonNumber]); |
| [super otherMouseDown:event]; |
| } |
| |
| // (URLDropTarget protocol) |
| - (id<URLDropTargetController>)urlDropController { |
| BrowserWindowController* windowController = |
| [BrowserWindowController browserWindowControllerForView:self]; |
| return [windowController toolbarController]; |
| } |
| |
| // (URLDropTarget protocol) |
| - (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender { |
| // Make ourself the first responder (even though we're presumably already the |
| // first responder), which will select the text to indicate that our contents |
| // would be replaced by a drop. |
| [[self window] makeFirstResponder:self]; |
| return [dropHandler_ draggingEntered:sender]; |
| } |
| |
| // (URLDropTarget protocol) |
| - (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)sender { |
| return [dropHandler_ draggingUpdated:sender]; |
| } |
| |
| // (URLDropTarget protocol) |
| - (void)draggingExited:(id<NSDraggingInfo>)sender { |
| return [dropHandler_ draggingExited:sender]; |
| } |
| |
| // (URLDropTarget protocol) |
| - (BOOL)performDragOperation:(id<NSDraggingInfo>)sender { |
| return [dropHandler_ performDragOperation:sender]; |
| } |
| |
| // Prevent control characters from being entered into the Omnibox. |
| // This is invoked for keyboard entry, not for pasting. |
| - (void)insertText:(id)aString { |
| // Repeatedly remove control characters. The loop will only ever |
| // execute at all when the user enters control characters (using |
| // Ctrl-Alt- or Ctrl-Q). Making this generally efficient would |
| // probably be a loss, since the input always seems to be a single |
| // character. |
| if ([aString isKindOfClass:[NSAttributedString class]]) { |
| NSRange range = |
| [[aString string] rangeOfCharacterFromSet:forbiddenCharacters_]; |
| while (range.location != NSNotFound) { |
| aString = [[aString mutableCopy] autorelease]; |
| [aString deleteCharactersInRange:range]; |
| range = [[aString string] rangeOfCharacterFromSet:forbiddenCharacters_]; |
| } |
| DCHECK_EQ(range.length, 0U); |
| } else { |
| NSRange range = [aString rangeOfCharacterFromSet:forbiddenCharacters_]; |
| while (range.location != NSNotFound) { |
| aString = |
| [aString stringByReplacingCharactersInRange:range withString:@""]; |
| range = [aString rangeOfCharacterFromSet:forbiddenCharacters_]; |
| } |
| DCHECK_EQ(range.length, 0U); |
| } |
| |
| // NOTE: If |aString| is empty, this intentionally replaces the |
| // selection with empty. This seems consistent with the case where |
| // the input contained a mixture of characters and the string ended |
| // up not empty. |
| [super insertText:aString]; |
| } |
| |
| - (void)setMarkedText:(id)aString selectedRange:(NSRange)selRange { |
| [super setMarkedText:aString selectedRange:selRange]; |
| |
| // Because the OmniboxViewMac class treats marked text as content, |
| // we need to treat the change to marked text as content change as well. |
| [self didChangeText]; |
| } |
| |
| - (NSRange)selectionRangeForProposedRange:(NSRange)proposedSelRange |
| granularity:(NSSelectionGranularity)granularity { |
| AutocompleteTextFieldObserver* observer = [self observer]; |
| NSRange modifiedRange = [super selectionRangeForProposedRange:proposedSelRange |
| granularity:granularity]; |
| if (observer) |
| return observer->SelectionRangeForProposedRange(modifiedRange); |
| return modifiedRange; |
| } |
| |
| |
| |
| |
| - (void)setSelectedRange:(NSRange)charRange |
| affinity:(NSSelectionAffinity)affinity |
| stillSelecting:(BOOL)flag { |
| [super setSelectedRange:charRange affinity:affinity stillSelecting:flag]; |
| |
| // We're only interested in selection changes directly caused by keyboard |
| // input from the user. |
| if (interpretingKeyEvents_) |
| textChangedByKeyEvents_ = YES; |
| } |
| |
| - (void)interpretKeyEvents:(NSArray *)eventArray { |
| DCHECK(!interpretingKeyEvents_); |
| interpretingKeyEvents_ = YES; |
| textChangedByKeyEvents_ = NO; |
| AutocompleteTextFieldObserver* observer = [self observer]; |
| |
| if (observer) |
| observer->OnBeforeChange(); |
| |
| [super interpretKeyEvents:eventArray]; |
| |
| if (textChangedByKeyEvents_ && observer) |
| observer->OnDidChange(); |
| |
| DCHECK(interpretingKeyEvents_); |
| interpretingKeyEvents_ = NO; |
| } |
| |
| - (BOOL)shouldChangeTextInRange:(NSRange)affectedCharRange |
| replacementString:(NSString *)replacementString { |
| BOOL ret = [super shouldChangeTextInRange:affectedCharRange |
| replacementString:replacementString]; |
| |
| if (ret && !interpretingKeyEvents_) { |
| AutocompleteTextFieldObserver* observer = [self observer]; |
| if (observer) |
| observer->OnBeforeChange(); |
| } |
| return ret; |
| } |
| |
| - (void)didChangeText { |
| [super didChangeText]; |
| |
| AutocompleteTextFieldObserver* observer = [self observer]; |
| if (observer) { |
| if (!interpretingKeyEvents_ && |
| ![[self undoManager] isUndoing] && ![[self undoManager] isRedoing]) { |
| observer->OnDidChange(); |
| } else if (interpretingKeyEvents_) { |
| textChangedByKeyEvents_ = YES; |
| } |
| } |
| } |
| |
| - (void)doCommandBySelector:(SEL)cmd { |
| // TODO(shess): Review code for cases where we're fruitlessly attempting to |
| // work in spite of not having an observer. |
| AutocompleteTextFieldObserver* observer = [self observer]; |
| |
| if (observer && observer->OnDoCommandBySelector(cmd)) { |
| // The observer should already be aware of any changes to the text, so |
| // setting |textChangedByKeyEvents_| to NO to prevent its OnDidChange() |
| // method from being called unnecessarily. |
| textChangedByKeyEvents_ = NO; |
| return; |
| } |
| |
| // If the escape key was pressed and no revert happened and we're in |
| // fullscreen mode, give focus to the web contents, which may dismiss the |
| // overlay. |
| if (cmd == @selector(cancelOperation:)) { |
| BrowserWindowController* windowController = |
| [BrowserWindowController browserWindowControllerForView:self]; |
| if ([windowController isFullscreen]) { |
| [windowController focusTabContents]; |
| textChangedByKeyEvents_ = NO; |
| return; |
| } |
| } |
| |
| [super doCommandBySelector:cmd]; |
| } |
| |
| - (void)setAttributedString:(NSAttributedString*)aString { |
| NSTextStorage* textStorage = [self textStorage]; |
| DCHECK(textStorage); |
| [textStorage setAttributedString:aString]; |
| |
| // The text has been changed programmatically. The observer should know |
| // this change, so setting |textChangedByKeyEvents_| to NO to |
| // prevent its OnDidChange() method from being called unnecessarily. |
| textChangedByKeyEvents_ = NO; |
| } |
| |
| - (BOOL)validateMenuItem:(NSMenuItem*)item { |
| if ([item action] == @selector(copyToFindPboard:)) |
| return [self selectedRange].length > 0; |
| if ([item action] == @selector(pasteAndGo:)) { |
| // TODO(rohitrao): If the clipboard is empty, should we show a |
| // greyed-out "Paste and Go" or nothing at all? |
| AutocompleteTextFieldObserver* observer = [self observer]; |
| DCHECK(observer); |
| return observer->CanPasteAndGo(); |
| } |
| if ([item action] == @selector(copyURL:)) { |
| AutocompleteTextFieldObserver* observer = [self observer]; |
| DCHECK(observer); |
| return observer->ShouldEnableCopyURL(); |
| } |
| return [super validateMenuItem:item]; |
| } |
| |
| - (void)copyToFindPboard:(id)sender { |
| NSRange selectedRange = [self selectedRange]; |
| if (selectedRange.length == 0) |
| return; |
| NSAttributedString* selection = |
| [self attributedSubstringForProposedRange:selectedRange |
| actualRange:NULL]; |
| if (!selection) |
| return; |
| |
| [[FindPasteboard sharedInstance] setFindText:[selection string]]; |
| } |
| |
| - (void)drawRect:(NSRect)rect { |
| [super drawRect:rect]; |
| autocomplete_text_field::DrawGrayTextAutocompletion( |
| [self textStorage], |
| [[self delegate] suggestText], |
| [[self delegate] suggestColor], |
| self, |
| [self bounds]); |
| } |
| |
| @end |