| // Copyright 2014 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 "ui/views_bridge_mac/bridged_content_view.h" |
| |
| #include "base/logging.h" |
| #import "base/mac/foundation_util.h" |
| #import "base/mac/mac_util.h" |
| #import "base/mac/scoped_nsobject.h" |
| #import "base/mac/sdk_forward_declarations.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/strings/sys_string_conversions.h" |
| #include "skia/ext/skia_utils_mac.h" |
| #import "ui/base/cocoa/appkit_utils.h" |
| #include "ui/base/cocoa/cocoa_base_utils.h" |
| #include "ui/base/dragdrop/drag_drop_types.h" |
| #include "ui/base/dragdrop/os_exchange_data_provider_mac.h" |
| #include "ui/base/ime/input_method.h" |
| #include "ui/base/ime/text_edit_commands.h" |
| #include "ui/base/ime/text_input_client.h" |
| #include "ui/compositor/canvas_painter.h" |
| #import "ui/events/cocoa/cocoa_event_utils.h" |
| #include "ui/events/event_utils.h" |
| #include "ui/events/keycodes/dom/dom_code.h" |
| #import "ui/events/keycodes/keyboard_code_conversion_mac.h" |
| #include "ui/gfx/canvas_paint_mac.h" |
| #include "ui/gfx/decorated_text.h" |
| #import "ui/gfx/decorated_text_mac.h" |
| #include "ui/gfx/geometry/rect.h" |
| #import "ui/gfx/mac/coordinate_conversion.h" |
| #import "ui/gfx/path_mac.h" |
| #include "ui/gfx/scoped_ns_graphics_context_save_gstate_mac.h" |
| #include "ui/views_bridge_mac/bridged_native_widget_host_helper.h" |
| #import "ui/views_bridge_mac/bridged_native_widget_impl.h" |
| #import "ui/views_bridge_mac/drag_drop_client.h" |
| #include "ui/views_bridge_mac/mojo/bridged_native_widget_host.mojom.h" |
| |
| namespace { |
| |
| NSString* const kFullKeyboardAccessChangedNotification = |
| @"com.apple.KeyboardUIModeDidChange"; |
| |
| // Convert a |point| in |source_window|'s AppKit coordinate system (origin at |
| // the bottom left of the window) to |target_window|'s content rect, with the |
| // origin at the top left of the content area. |
| // If |source_window| is nil, |point| will be treated as screen coordinates. |
| gfx::Point MovePointToWindow(const NSPoint& point, |
| NSWindow* source_window, |
| NSWindow* target_window) { |
| NSPoint point_in_screen = |
| source_window ? ui::ConvertPointFromWindowToScreen(source_window, point) |
| : point; |
| |
| NSPoint point_in_window = |
| ui::ConvertPointFromScreenToWindow(target_window, point_in_screen); |
| NSRect content_rect = |
| [target_window contentRectForFrameRect:[target_window frame]]; |
| return gfx::Point(point_in_window.x, |
| NSHeight(content_rect) - point_in_window.y); |
| } |
| |
| // Returns true if |event| may have triggered dismissal of an IME and would |
| // otherwise be ignored by a ui::TextInputClient when inserted. |
| bool IsImeTriggerEvent(NSEvent* event) { |
| ui::KeyboardCode key = ui::KeyboardCodeFromNSEvent(event); |
| return key == ui::VKEY_RETURN || key == ui::VKEY_TAB || |
| key == ui::VKEY_ESCAPE; |
| } |
| |
| ui::TextEditCommand GetTextEditCommandForMenuAction(SEL action) { |
| if (action == @selector(undo:)) |
| return ui::TextEditCommand::UNDO; |
| if (action == @selector(redo:)) |
| return ui::TextEditCommand::REDO; |
| if (action == @selector(cut:)) |
| return ui::TextEditCommand::CUT; |
| if (action == @selector(copy:)) |
| return ui::TextEditCommand::COPY; |
| if (action == @selector(paste:)) |
| return ui::TextEditCommand::PASTE; |
| if (action == @selector(pasteAndMatchStyle:)) |
| return ui::TextEditCommand::PASTE; |
| if (action == @selector(selectAll:)) |
| return ui::TextEditCommand::SELECT_ALL; |
| return ui::TextEditCommand::INVALID_COMMAND; |
| } |
| |
| } // namespace |
| |
| @interface BridgedContentView () |
| |
| // Dispatch |event| to |bridge_|'s host. |
| - (void)dispatchKeyEvent:(ui::KeyEvent*)event; |
| |
| // Returns true if active menu controller corresponds to this widget. Note that |
| // this will synchronously call into the browser process. |
| - (BOOL)hasActiveMenuController; |
| |
| // Dispatch |event| to |menu_controller| and return true if |event| is |
| // swallowed. |
| - (BOOL)dispatchKeyEventToMenuController:(ui::KeyEvent*)event; |
| |
| // Passes |event| to the InputMethod for dispatch. |
| - (void)handleKeyEvent:(ui::KeyEvent*)event; |
| |
| // Allows accelerators to be handled at different points in AppKit key event |
| // dispatch. Checks for an unhandled event passed in to -keyDown: and passes it |
| // to the Widget for processing. Returns YES if the Widget handles it. |
| - (BOOL)handleUnhandledKeyDownAsKeyEvent; |
| |
| // Handles an NSResponder Action Message by mapping it to a corresponding text |
| // editing command from ui_strings.grd and, when not being sent to a |
| // TextInputClient, the keyCode that toolkit-views expects internally. |
| // For example, moveToLeftEndOfLine: would pass ui::VKEY_HOME in non-RTL locales |
| // even though the Home key on Mac defaults to moveToBeginningOfDocument:. |
| // This approach also allows action messages a user |
| // may have remapped in ~/Library/KeyBindings/DefaultKeyBinding.dict to be |
| // catered for. |
| // Note: default key bindings in Mac can be read from StandardKeyBinding.dict |
| // which lives in /System/Library/Frameworks/AppKit.framework/Resources. Do |
| // `plutil -convert xml1 -o StandardKeyBinding.xml StandardKeyBinding.dict` to |
| // get something readable. |
| - (void)handleAction:(ui::TextEditCommand)command |
| keyCode:(ui::KeyboardCode)keyCode |
| domCode:(ui::DomCode)domCode |
| eventFlags:(int)eventFlags; |
| |
| // ui::EventLocationFromNative() assumes the event hit the contentView. |
| // Adjust |event| if that's not the case (e.g. for reparented views). |
| - (void)adjustUiEventLocation:(ui::LocatedEvent*)event |
| fromNativeEvent:(NSEvent*)nativeEvent; |
| |
| // Notification handler invoked when the Full Keyboard Access mode is changed. |
| - (void)onFullKeyboardAccessModeChanged:(NSNotification*)notification; |
| |
| // Helper method which forwards |text| to the active menu or |textInputClient_|. |
| - (void)insertTextInternal:(id)text; |
| |
| // Returns the native Widget's drag drop client. Possibly null. |
| - (views_bridge_mac::DragDropClient*)dragDropClient NS_RETURNS_INNER_POINTER; |
| |
| // Returns true if there exists a ui::TextInputClient for the currently focused |
| // views::View. |
| - (BOOL)hasTextInputClient; |
| |
| // Returns true if there exists a ui::TextInputClient for the currently focused |
| // views::View and that client is right-to-left. |
| - (BOOL)isTextRTL; |
| |
| // Menu action handlers. |
| - (void)undo:(id)sender; |
| - (void)redo:(id)sender; |
| - (void)cut:(id)sender; |
| - (void)copy:(id)sender; |
| - (void)paste:(id)sender; |
| - (void)pasteAndMatchStyle:(id)sender; |
| - (void)selectAll:(id)sender; |
| |
| @end |
| |
| @implementation BridgedContentView |
| |
| @synthesize bridge = bridge_; |
| @synthesize drawMenuBackgroundForBlur = drawMenuBackgroundForBlur_; |
| |
| - (instancetype)initWithBridge:(views::BridgedNativeWidgetImpl*)bridge |
| bounds:(gfx::Rect)bounds { |
| // To keep things simple, assume the origin is (0, 0) until there exists a use |
| // case for something other than that. |
| DCHECK(bounds.origin().IsOrigin()); |
| NSRect initialFrame = NSMakeRect(0, 0, bounds.width(), bounds.height()); |
| if ((self = [super initWithFrame:initialFrame])) { |
| bridge_ = bridge; |
| |
| // Apple's documentation says that NSTrackingActiveAlways is incompatible |
| // with NSTrackingCursorUpdate, so use NSTrackingActiveInActiveApp. |
| cursorTrackingArea_.reset([[CrTrackingArea alloc] |
| initWithRect:NSZeroRect |
| options:NSTrackingMouseMoved | NSTrackingCursorUpdate | |
| NSTrackingActiveInActiveApp | NSTrackingInVisibleRect | |
| NSTrackingMouseEnteredAndExited |
| owner:self |
| userInfo:nil]); |
| [self addTrackingArea:cursorTrackingArea_.get()]; |
| |
| // Get notified whenever Full Keyboard Access mode is changed. |
| [[NSDistributedNotificationCenter defaultCenter] |
| addObserver:self |
| selector:@selector(onFullKeyboardAccessModeChanged:) |
| name:kFullKeyboardAccessChangedNotification |
| object:nil]; |
| |
| // Initialize the focus manager with the correct keyboard accessibility |
| // setting. |
| [self updateFullKeyboardAccess]; |
| [self registerForDraggedTypes:ui::OSExchangeDataProviderMac:: |
| SupportedPasteboardTypes()]; |
| } |
| return self; |
| } |
| |
| - (ui::TextInputClient*)textInputClient { |
| return bridge_ ? bridge_->host_helper()->GetTextInputClient() : nullptr; |
| } |
| |
| - (BOOL)hasTextInputClient { |
| bool hasTextInputClient = NO; |
| if (bridge_) |
| bridge_->text_input_host()->HasClient(&hasTextInputClient); |
| return hasTextInputClient; |
| } |
| |
| - (BOOL)isTextRTL { |
| bool isRTL = NO; |
| if (bridge_) |
| bridge_->text_input_host()->IsRTL(&isRTL); |
| return isRTL; |
| } |
| |
| - (void)dealloc { |
| // By the time |self| is dealloc'd, it should never be in an NSWindow, and it |
| // should never be the current input context. |
| DCHECK_EQ(nil, [self window]); |
| // Sanity check: NSView always provides an -inputContext. |
| DCHECK_NE(nil, [super inputContext]); |
| DCHECK_NE([NSTextInputContext currentInputContext], [super inputContext]); |
| [super dealloc]; |
| } |
| |
| - (void)clearView { |
| bridge_ = nullptr; |
| [[NSDistributedNotificationCenter defaultCenter] removeObserver:self]; |
| [cursorTrackingArea_.get() clearOwner]; |
| [self removeTrackingArea:cursorTrackingArea_.get()]; |
| } |
| |
| - (bool)needsUpdateWindows { |
| // If |self| was being used for the input context, and would now report a |
| // different input context, manually invoke [NSApp updateWindows]. This is |
| // necessary because AppKit holds on to a raw pointer to a NSTextInputContext |
| // (which may have been the one returned by [self inputContext]) that is only |
| // updated by -updateWindows. And although AppKit invokes that on each |
| // iteration through most runloop modes, it does not call it when running |
| // NSEventTrackingRunLoopMode, and not _within_ a run loop iteration, where |
| // the inputContext may change before further event processing. |
| NSTextInputContext* current = [NSTextInputContext currentInputContext]; |
| if (!current) |
| return false; |
| |
| NSTextInputContext* newContext = [self inputContext]; |
| // If the newContext is non-nil, then it can only be [super inputContext]. So |
| // the input context is either not changing, or it was not from |self|. In |
| // both cases, there's no need to call -updateWindows. |
| if (newContext) { |
| DCHECK_EQ(newContext, [super inputContext]); |
| return false; |
| } |
| |
| return current == [super inputContext]; |
| } |
| |
| // If |point| is classified as a draggable background (HTCAPTION), return nil so |
| // that it can lead to a window drag or double-click in the title bar. Dragging |
| // could be optimized by telling the window server which regions should be |
| // instantly draggable without asking (tracked at https://crbug.com/830962). |
| - (NSView*)hitTest:(NSPoint)point { |
| gfx::Point flippedPoint(point.x, NSHeight(self.superview.bounds) - point.y); |
| bool isDraggableBackground = false; |
| bridge_->host()->GetIsDraggableBackgroundAt(flippedPoint, |
| &isDraggableBackground); |
| if (isDraggableBackground) |
| return nil; |
| return [super hitTest:point]; |
| } |
| |
| - (void)processCapturedMouseEvent:(NSEvent*)theEvent { |
| if (!bridge_) |
| return; |
| |
| NSWindow* source = [theEvent window]; |
| NSWindow* target = [self window]; |
| DCHECK(target); |
| |
| BOOL isScrollEvent = [theEvent type] == NSScrollWheel; |
| |
| // If it's the view's window, process normally. |
| if ([target isEqual:source]) { |
| if (isScrollEvent) |
| [self scrollWheel:theEvent]; |
| else |
| [self mouseEvent:theEvent]; |
| |
| return; |
| } |
| |
| gfx::Point event_location = |
| MovePointToWindow([theEvent locationInWindow], source, target); |
| [self updateTooltipIfRequiredAt:event_location]; |
| |
| if (isScrollEvent) { |
| auto event = std::make_unique<ui::ScrollEvent>(theEvent); |
| event->set_location(event_location); |
| bridge_->host()->OnScrollEvent(std::move(event)); |
| } else { |
| auto event = std::make_unique<ui::MouseEvent>(theEvent); |
| event->set_location(event_location); |
| bridge_->host()->OnMouseEvent(std::move(event)); |
| } |
| } |
| |
| - (void)updateTooltipIfRequiredAt:(const gfx::Point&)locationInContent { |
| DCHECK(bridge_); |
| base::string16 newTooltipText; |
| |
| bridge_->host()->GetTooltipTextAt(locationInContent, &newTooltipText); |
| if (newTooltipText != lastTooltipText_) { |
| std::swap(newTooltipText, lastTooltipText_); |
| [self setToolTipAtMousePoint:base::SysUTF16ToNSString(lastTooltipText_)]; |
| } |
| } |
| |
| - (void)updateFullKeyboardAccess { |
| if (!bridge_) |
| return; |
| bridge_->host()->SetKeyboardAccessible([NSApp isFullKeyboardAccessEnabled]); |
| } |
| |
| // BridgedContentView private implementation. |
| |
| - (void)dispatchKeyEvent:(ui::KeyEvent*)event { |
| if (bridge_) |
| bridge_->host_helper()->DispatchKeyEvent(event); |
| } |
| |
| - (BOOL)hasActiveMenuController { |
| bool hasMenuController = false; |
| if (bridge_) |
| bridge_->host()->GetHasMenuController(&hasMenuController); |
| return hasMenuController; |
| } |
| |
| - (BOOL)dispatchKeyEventToMenuController:(ui::KeyEvent*)event { |
| if (bridge_) |
| return bridge_->host_helper()->DispatchKeyEventToMenuController(event); |
| return false; |
| } |
| |
| - (void)handleKeyEvent:(ui::KeyEvent*)event { |
| DCHECK(event); |
| if ([self dispatchKeyEventToMenuController:event]) |
| return; |
| |
| [self dispatchKeyEvent:event]; |
| } |
| |
| - (BOOL)handleUnhandledKeyDownAsKeyEvent { |
| if (!hasUnhandledKeyDownEvent_) |
| return NO; |
| |
| ui::KeyEvent event(keyDownEvent_); |
| [self handleKeyEvent:&event]; |
| hasUnhandledKeyDownEvent_ = NO; |
| return event.handled(); |
| } |
| |
| - (void)handleAction:(ui::TextEditCommand)command |
| keyCode:(ui::KeyboardCode)keyCode |
| domCode:(ui::DomCode)domCode |
| eventFlags:(int)eventFlags { |
| if (!bridge_) |
| return; |
| |
| // Always propagate the shift modifier if present. Shift doesn't always alter |
| // the command selector, but should always be passed along. Control and Alt |
| // have different meanings on Mac, so they do not propagate automatically. |
| if ([keyDownEvent_ modifierFlags] & NSShiftKeyMask) |
| eventFlags |= ui::EF_SHIFT_DOWN; |
| |
| // Generate a synthetic event with the keycode toolkit-views expects. |
| ui::KeyEvent event(ui::ET_KEY_PRESSED, keyCode, domCode, eventFlags); |
| |
| if ([self dispatchKeyEventToMenuController:&event]) |
| return; |
| |
| // If there's an active TextInputClient, schedule the editing command to be |
| // performed. |
| // TODO(https://crbug.com/901490): Add mojo support for ui::TextEditCommand. |
| if ([self textInputClient] && |
| [self textInputClient] -> IsTextEditCommandEnabled(command)) { |
| [self textInputClient] -> SetTextEditCommandForNextKeyEvent(command); |
| } |
| |
| [self dispatchKeyEvent:&event]; |
| } |
| |
| - (void)adjustUiEventLocation:(ui::LocatedEvent*)event |
| fromNativeEvent:(NSEvent*)nativeEvent { |
| if ([nativeEvent window] && [[self window] contentView] != self) { |
| NSPoint p = [self convertPoint:[nativeEvent locationInWindow] fromView:nil]; |
| event->set_location(gfx::Point(p.x, NSHeight([self frame]) - p.y)); |
| } |
| } |
| |
| - (void)onFullKeyboardAccessModeChanged:(NSNotification*)notification { |
| DCHECK([[notification name] |
| isEqualToString:kFullKeyboardAccessChangedNotification]); |
| [self updateFullKeyboardAccess]; |
| } |
| |
| - (void)insertTextInternal:(id)text { |
| if (!bridge_) |
| return; |
| |
| if ([text isKindOfClass:[NSAttributedString class]]) |
| text = [text string]; |
| |
| bool isCharacterEvent = keyDownEvent_ && [text length] == 1; |
| // Pass "character" events to the View hierarchy. Cases this handles (non- |
| // exhaustive)- |
| // - Space key press on controls. Unlike Tab and newline which have |
| // corresponding action messages, an insertText: message is generated for |
| // the Space key (insertText:replacementRange: when there's an active |
| // input context). |
| // - Menu mnemonic selection. |
| // Note we create a custom character ui::KeyEvent (and not use the |
| // ui::KeyEvent(NSEvent*) constructor) since we can't just rely on the event |
| // key code to get the actual characters from the ui::KeyEvent. This for |
| // example is necessary for menu mnemonic selection of non-latin text. |
| |
| // Don't generate a key event when there is marked composition text. These key |
| // down events should be consumed by the IME and not reach the Views layer. |
| // For example, on pressing Return to commit composition text, if we passed a |
| // synthetic key event to the View hierarchy, it will have the effect of |
| // performing the default action on the current dialog. We do not want this |
| // when there is marked text (Return should only confirm the IME). |
| |
| // However, IME for phonetic languages such as Korean do not always _mark_ |
| // text when a composition is active. For these, correct behaviour is to |
| // handle the final -keyDown: that caused the composition to be committed, but |
| // only _after_ the sequence of insertText: messages coming from IME have been |
| // sent to the TextInputClient. Detect this by comparing to -[NSEvent |
| // characters]. Note we do not use -charactersIgnoringModifiers: so that, |
| // e.g., ß (Alt+s) will match mnemonics with ß rather than s. |
| bool isFinalInsertForKeyEvent = |
| isCharacterEvent && [text isEqualToString:[keyDownEvent_ characters]]; |
| |
| // Also note that a single, non-IME key down event can also cause multiple |
| // insertText:replacementRange: action messages being generated from within |
| // -keyDown:'s call to -interpretKeyEvents:. One example, on pressing Alt+e, |
| // the accent (´) character is composed via setMarkedText:. Now on pressing |
| // the character 'r', two insertText:replacementRange: action messages are |
| // generated with the text value of accent (´) and 'r' respectively. The key |
| // down event will have characters field of length 2. The first of these |
| // insertText messages won't generate a KeyEvent since there'll be active |
| // marked text. However, a KeyEvent will be generated corresponding to 'r'. |
| |
| // Currently there seems to be no use case to pass non-character events routed |
| // from insertText: handlers to the View hierarchy. |
| if (isFinalInsertForKeyEvent && ![self hasMarkedText]) { |
| ui::KeyEvent charEvent([text characterAtIndex:0], |
| ui::KeyboardCodeFromNSEvent(keyDownEvent_), |
| ui::DomCodeFromNSEvent(keyDownEvent_), ui::EF_NONE); |
| [self handleKeyEvent:&charEvent]; |
| hasUnhandledKeyDownEvent_ = NO; |
| if (charEvent.handled()) |
| return; |
| } |
| |
| // Forward the |text| to |textInputClient_| if no menu is active. |
| if ([self hasTextInputClient] && ![self hasActiveMenuController]) { |
| // Note we don't check isFinalInsertForKeyEvent, nor use |keyDownEvent_| |
| // to generate the synthetic ui::KeyEvent since: For composed text, |
| // [keyDownEvent_ characters] might not be the same as |text|. This is |
| // because |keyDownEvent_| will correspond to the event that caused the |
| // composition text to be confirmed, say, Return key press. |
| bridge_->text_input_host()->InsertText(base::SysNSStringToUTF16(text), |
| isCharacterEvent); |
| // Suppress accelerators that may be bound to this key, since it inserted |
| // text instead. But note that IME may follow with -insertNewLine:, which |
| // will resurrect the keyEvent for accelerator handling. |
| hasUnhandledKeyDownEvent_ = NO; |
| } |
| } |
| |
| - (views_bridge_mac::DragDropClient*)dragDropClient { |
| return bridge_ ? bridge_->drag_drop_client() : nullptr; |
| } |
| |
| - (void)undo:(id)sender { |
| [self handleAction:ui::TextEditCommand::UNDO |
| keyCode:ui::VKEY_Z |
| domCode:ui::DomCode::US_Z |
| eventFlags:ui::EF_CONTROL_DOWN]; |
| } |
| |
| - (void)redo:(id)sender { |
| [self handleAction:ui::TextEditCommand::REDO |
| keyCode:ui::VKEY_Z |
| domCode:ui::DomCode::US_Z |
| eventFlags:ui::EF_CONTROL_DOWN | ui::EF_SHIFT_DOWN]; |
| } |
| |
| - (void)cut:(id)sender { |
| [self handleAction:ui::TextEditCommand::CUT |
| keyCode:ui::VKEY_X |
| domCode:ui::DomCode::US_X |
| eventFlags:ui::EF_CONTROL_DOWN]; |
| } |
| |
| - (void)copy:(id)sender { |
| [self handleAction:ui::TextEditCommand::COPY |
| keyCode:ui::VKEY_C |
| domCode:ui::DomCode::US_C |
| eventFlags:ui::EF_CONTROL_DOWN]; |
| } |
| |
| - (void)paste:(id)sender { |
| [self handleAction:ui::TextEditCommand::PASTE |
| keyCode:ui::VKEY_V |
| domCode:ui::DomCode::US_V |
| eventFlags:ui::EF_CONTROL_DOWN]; |
| } |
| |
| - (void)pasteAndMatchStyle:(id)sender { |
| [self handleAction:ui::TextEditCommand::PASTE |
| keyCode:ui::VKEY_V |
| domCode:ui::DomCode::US_V |
| eventFlags:ui::EF_CONTROL_DOWN | ui::EF_SHIFT_DOWN]; |
| } |
| |
| - (void)selectAll:(id)sender { |
| [self handleAction:ui::TextEditCommand::SELECT_ALL |
| keyCode:ui::VKEY_A |
| domCode:ui::DomCode::US_A |
| eventFlags:ui::EF_CONTROL_DOWN]; |
| } |
| |
| // BaseView implementation. |
| |
| // Don't use tracking areas from BaseView. BridgedContentView's tracks |
| // NSTrackingCursorUpdate and Apple's documentation suggests it's incompatible. |
| - (void)enableTracking { |
| } |
| |
| // Translates the location of |theEvent| to toolkit-views coordinates and passes |
| // the event to NativeWidgetMac for handling. |
| - (void)mouseEvent:(NSEvent*)theEvent { |
| if (!bridge_) |
| return; |
| |
| DCHECK([theEvent type] != NSScrollWheel); |
| auto event = std::make_unique<ui::MouseEvent>(theEvent); |
| [self adjustUiEventLocation:event.get() fromNativeEvent:theEvent]; |
| |
| // Aura updates tooltips with the help of aura::Window::AddPreTargetHandler(). |
| // Mac hooks in here. |
| [self updateTooltipIfRequiredAt:event->location()]; |
| bridge_->host()->OnMouseEvent(std::move(event)); |
| } |
| |
| - (void)forceTouchEvent:(NSEvent*)theEvent { |
| if (ui::ForceClickInvokesQuickLook()) |
| [self quickLookWithEvent:theEvent]; |
| } |
| |
| // NSView implementation. |
| |
| // This view must consistently return YES or else dragging a tab may drag the |
| // entire window. See r549802 for details. |
| - (BOOL)acceptsFirstResponder { |
| return YES; |
| } |
| |
| - (BOOL)becomeFirstResponder { |
| if ([[self window] firstResponder] != self) |
| return NO; |
| BOOL result = [super becomeFirstResponder]; |
| if (result && bridge_) |
| bridge_->host()->OnIsFirstResponderChanged(true); |
| return result; |
| } |
| |
| - (BOOL)resignFirstResponder { |
| BOOL result = [super resignFirstResponder]; |
| if (result && bridge_) |
| bridge_->host()->OnIsFirstResponderChanged(false); |
| return result; |
| } |
| |
| - (void)viewDidMoveToWindow { |
| // When this view is added to a window, AppKit calls setFrameSize before it is |
| // added to the window, so the behavior in setFrameSize is not triggered. |
| NSWindow* window = [self window]; |
| if (window) |
| [self setFrameSize:NSZeroSize]; |
| } |
| |
| - (void)setFrameSize:(NSSize)newSize { |
| // The size passed in here does not always use |
| // -[NSWindow contentRectForFrameRect]. The following ensures that the |
| // contentView for a frameless window can extend over the titlebar of the new |
| // window containing it, since AppKit requires a titlebar to give frameless |
| // windows correct shadows and rounded corners. |
| NSWindow* window = [self window]; |
| if (window && [window contentView] == self) { |
| newSize = [window contentRectForFrameRect:[window frame]].size; |
| // Ensure that the window geometry be updated on the host side before the |
| // view size is updated. |
| // TODO(ccameron): Consider updating the view size and window size and |
| // position together in UpdateWindowGeometry. |
| // https://crbug.com/875776, https://crbug.com/875731 |
| if (bridge_) |
| bridge_->UpdateWindowGeometry(); |
| } |
| |
| [super setFrameSize:newSize]; |
| |
| if (bridge_) |
| bridge_->host()->OnViewSizeChanged( |
| gfx::Size(newSize.width, newSize.height)); |
| } |
| |
| - (BOOL)isOpaque { |
| return bridge_ ? !bridge_->is_translucent_window() : NO; |
| } |
| |
| // To maximize consistency with the Cocoa browser (mac_views_browser=0), accept |
| // mouse clicks immediately so that clicking on Chrome from an inactive window |
| // will allow the event to be processed, rather than merely activate the window. |
| - (BOOL)acceptsFirstMouse:(NSEvent*)theEvent { |
| return YES; |
| } |
| |
| // NSDraggingDestination protocol overrides. |
| |
| - (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender { |
| return [self draggingUpdated:sender]; |
| } |
| |
| - (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)sender { |
| views_bridge_mac::DragDropClient* client = [self dragDropClient]; |
| const auto drag_operation = |
| client ? client->DragUpdate(sender) : ui::DragDropTypes::DRAG_NONE; |
| UMA_HISTOGRAM_BOOLEAN("Event.DragDrop.AcceptDragUpdate", |
| drag_operation != ui::DragDropTypes::DRAG_NONE); |
| return drag_operation; |
| } |
| |
| - (void)draggingExited:(id<NSDraggingInfo>)sender { |
| views_bridge_mac::DragDropClient* client = [self dragDropClient]; |
| if (client) |
| client->DragExit(); |
| } |
| |
| - (BOOL)performDragOperation:(id<NSDraggingInfo>)sender { |
| views_bridge_mac::DragDropClient* client = [self dragDropClient]; |
| return client && client->Drop(sender) != NSDragOperationNone; |
| } |
| |
| - (NSTextInputContext*)inputContext { |
| if (!bridge_) |
| return nil; |
| bool hasTextInputContext = false; |
| bridge_->text_input_host()->HasInputContext(&hasTextInputContext); |
| return hasTextInputContext ? [super inputContext] : nil; |
| } |
| |
| // NSResponder implementation. |
| |
| - (BOOL)_wantsKeyDownForEvent:(NSEvent*)event { |
| // This is a SPI that AppKit apparently calls after |performKeyEquivalent:| |
| // returned NO. If this function returns |YES|, Cocoa sends the event to |
| // |keyDown:| instead of doing other things with it. Ctrl-tab will be sent |
| // to us instead of doing key view loop control, ctrl-left/right get handled |
| // correctly, etc. |
| // (However, there are still some keys that Cocoa swallows, e.g. the key |
| // equivalent that Cocoa uses for toggling the input language. In this case, |
| // that's actually a good thing, though -- see http://crbug.com/26115 .) |
| return YES; |
| } |
| |
| - (void)keyDown:(NSEvent*)theEvent { |
| BOOL hadMarkedTextAtKeyDown = [self hasMarkedText]; |
| |
| // Convert the event into an action message, according to OSX key mappings. |
| keyDownEvent_ = theEvent; |
| hasUnhandledKeyDownEvent_ = YES; |
| wantsKeyHandledForInsert_ = NO; |
| [self interpretKeyEvents:@[ theEvent ]]; |
| |
| // When there is marked text, -[NSView interpretKeyEvents:] may handle the |
| // event by dismissing the IME window in a way that neither unmarks text, nor |
| // updates any composition. That is, no signal is given either to the |
| // NSTextInputClient or the NSTextInputContext that the IME changed state. |
| // However, we must ensure this key down is not processed as an accelerator. |
| // TODO(tapted): Investigate removing the IsImeTriggerEvent() check - it's |
| // probably not required, but helps tests that expect some events to always |
| // get processed (i.e. TextfieldTest.TextInputClientTest). |
| if (hadMarkedTextAtKeyDown && IsImeTriggerEvent(theEvent)) |
| hasUnhandledKeyDownEvent_ = NO; |
| |
| // Even with marked text, some IMEs may follow with -insertNewLine:; |
| // simultaneously confirming the composition. In this case, always generate |
| // the corresponding ui::KeyEvent. Note this is done even if there was no |
| // marked text, so it is orthogonal to the case above. |
| if (wantsKeyHandledForInsert_) |
| hasUnhandledKeyDownEvent_ = YES; |
| |
| // If |hasUnhandledKeyDownEvent_| wasn't set to NO during |
| // -interpretKeyEvents:, it wasn't handled. Give Widget accelerators a chance |
| // to handle it. |
| [self handleUnhandledKeyDownAsKeyEvent]; |
| DCHECK(!hasUnhandledKeyDownEvent_); |
| keyDownEvent_ = nil; |
| } |
| |
| - (void)keyUp:(NSEvent*)theEvent { |
| ui::KeyEvent event(theEvent); |
| [self handleKeyEvent:&event]; |
| } |
| |
| - (void)flagsChanged:(NSEvent*)theEvent { |
| if (theEvent.keyCode == 0) { |
| // An event like this gets sent when sending some key commands via |
| // AppleScript. Since 0 is VKEY_A, we end up interpreting this as Cmd+A |
| // which is incorrect. The correct event for command up/down (keyCode = 55) |
| // is also sent, so we should drop this one. See https://crbug.com/889618 |
| return; |
| } |
| ui::KeyEvent event(theEvent); |
| [self handleKeyEvent:&event]; |
| } |
| |
| - (void)scrollWheel:(NSEvent*)theEvent { |
| if (!bridge_) |
| return; |
| |
| auto event = std::make_unique<ui::ScrollEvent>(theEvent); |
| [self adjustUiEventLocation:event.get() fromNativeEvent:theEvent]; |
| |
| // Aura updates tooltips with the help of aura::Window::AddPreTargetHandler(). |
| // Mac hooks in here. |
| [self updateTooltipIfRequiredAt:event->location()]; |
| bridge_->host()->OnScrollEvent(std::move(event)); |
| } |
| |
| // Called when we get a three-finger swipe, and they're enabled in System |
| // Preferences. |
| - (void)swipeWithEvent:(NSEvent*)event { |
| if (!bridge_) |
| return; |
| |
| // themblsha: In my testing all three-finger swipes send only a single event |
| // with a value of +/-1 on either axis. Diagonal swipes are not handled by |
| // AppKit. |
| |
| // We need to invert deltas in order to match GestureEventDetails's |
| // directions. |
| ui::GestureEventDetails swipeDetails(ui::ET_GESTURE_SWIPE, -[event deltaX], |
| -[event deltaY]); |
| swipeDetails.set_device_type(ui::GestureDeviceType::DEVICE_TOUCHPAD); |
| swipeDetails.set_touch_points(3); |
| |
| gfx::PointF location = ui::EventLocationFromNative(event); |
| // Note this uses the default unique_touch_event_id of 0 (Swipe events do not |
| // support -[NSEvent eventNumber]). This doesn't seem like a real omission |
| // because the three-finger swipes are instant and can't be tracked anyway. |
| auto gestureEvent = std::make_unique<ui::GestureEvent>( |
| location.x(), location.y(), ui::EventFlagsFromNative(event), |
| ui::EventTimeFromNative(event), swipeDetails); |
| bridge_->host()->OnGestureEvent(std::move(gestureEvent)); |
| } |
| |
| - (void)quickLookWithEvent:(NSEvent*)theEvent { |
| if (!bridge_) |
| return; |
| |
| const gfx::Point locationInContent = |
| gfx::ToFlooredPoint(ui::EventLocationFromNative(theEvent)); |
| |
| bool foundWord = false; |
| gfx::DecoratedText decoratedWord; |
| gfx::Point baselinePoint; |
| bridge_->host_helper()->GetWordAt(locationInContent, &foundWord, |
| &decoratedWord, &baselinePoint); |
| if (!foundWord) |
| return; |
| |
| NSPoint baselinePointAppKit = NSMakePoint( |
| baselinePoint.x(), NSHeight([self frame]) - baselinePoint.y()); |
| [self showDefinitionForAttributedString: |
| gfx::GetAttributedStringFromDecoratedText(decoratedWord) |
| atPoint:baselinePointAppKit]; |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // NSResponder Action Messages. Keep sorted according NSResponder.h (from the |
| // 10.9 SDK). The list should eventually be complete. Anything not defined will |
| // beep when interpretKeyEvents: would otherwise call it. |
| // TODO(tapted): Make this list complete, except for insert* methods which are |
| // dispatched as regular key events in doCommandBySelector:. |
| |
| // views::Textfields are single-line only, map Paragraph and Document commands |
| // to Line. Also, Up/Down commands correspond to beginning/end of line. |
| |
| // The insertText action message forwards to the TextInputClient unless a menu |
| // is active. Note that NSResponder's interpretKeyEvents: implementation doesn't |
| // direct insertText: through doCommandBySelector:, so this is still needed to |
| // handle the case when inputContext: is nil. When inputContext: returns non-nil |
| // text goes directly to insertText:replacementRange:. |
| - (void)insertText:(id)text { |
| DCHECK_EQ(nil, [self inputContext]); |
| [self insertTextInternal:text]; |
| } |
| |
| // Selection movement and scrolling. |
| |
| - (void)moveForward:(id)sender { |
| [self handleAction:ui::TextEditCommand::MOVE_FORWARD |
| keyCode:ui::VKEY_UNKNOWN |
| domCode:ui::DomCode::NONE |
| eventFlags:0]; |
| } |
| |
| - (void)moveRight:(id)sender { |
| [self handleAction:ui::TextEditCommand::MOVE_RIGHT |
| keyCode:ui::VKEY_RIGHT |
| domCode:ui::DomCode::ARROW_RIGHT |
| eventFlags:0]; |
| } |
| |
| - (void)moveBackward:(id)sender { |
| [self handleAction:ui::TextEditCommand::MOVE_BACKWARD |
| keyCode:ui::VKEY_UNKNOWN |
| domCode:ui::DomCode::NONE |
| eventFlags:0]; |
| } |
| |
| - (void)moveLeft:(id)sender { |
| [self handleAction:ui::TextEditCommand::MOVE_LEFT |
| keyCode:ui::VKEY_LEFT |
| domCode:ui::DomCode::ARROW_LEFT |
| eventFlags:0]; |
| } |
| |
| - (void)moveUp:(id)sender { |
| [self handleAction:ui::TextEditCommand::MOVE_UP |
| keyCode:ui::VKEY_UP |
| domCode:ui::DomCode::ARROW_UP |
| eventFlags:0]; |
| } |
| |
| - (void)moveDown:(id)sender { |
| [self handleAction:ui::TextEditCommand::MOVE_DOWN |
| keyCode:ui::VKEY_DOWN |
| domCode:ui::DomCode::ARROW_DOWN |
| eventFlags:0]; |
| } |
| |
| - (void)moveWordForward:(id)sender { |
| [self handleAction:ui::TextEditCommand::MOVE_WORD_FORWARD |
| keyCode:ui::VKEY_UNKNOWN |
| domCode:ui::DomCode::NONE |
| eventFlags:0]; |
| } |
| |
| - (void)moveWordBackward:(id)sender { |
| [self handleAction:ui::TextEditCommand::MOVE_WORD_BACKWARD |
| keyCode:ui::VKEY_UNKNOWN |
| domCode:ui::DomCode::NONE |
| eventFlags:0]; |
| } |
| |
| - (void)moveToBeginningOfLine:(id)sender { |
| [self handleAction:ui::TextEditCommand::MOVE_TO_BEGINNING_OF_LINE |
| keyCode:ui::VKEY_HOME |
| domCode:ui::DomCode::HOME |
| eventFlags:0]; |
| } |
| |
| - (void)moveToEndOfLine:(id)sender { |
| [self handleAction:ui::TextEditCommand::MOVE_TO_END_OF_LINE |
| keyCode:ui::VKEY_END |
| domCode:ui::DomCode::END |
| eventFlags:0]; |
| } |
| |
| - (void)moveToBeginningOfParagraph:(id)sender { |
| [self handleAction:ui::TextEditCommand::MOVE_TO_BEGINNING_OF_PARAGRAPH |
| keyCode:ui::VKEY_UNKNOWN |
| domCode:ui::DomCode::NONE |
| eventFlags:0]; |
| } |
| |
| - (void)moveToEndOfParagraph:(id)sender { |
| [self handleAction:ui::TextEditCommand::MOVE_TO_END_OF_PARAGRAPH |
| keyCode:ui::VKEY_UNKNOWN |
| domCode:ui::DomCode::NONE |
| eventFlags:0]; |
| } |
| |
| - (void)moveToEndOfDocument:(id)sender { |
| [self handleAction:ui::TextEditCommand::MOVE_TO_END_OF_DOCUMENT |
| keyCode:ui::VKEY_END |
| domCode:ui::DomCode::END |
| eventFlags:ui::EF_CONTROL_DOWN]; |
| } |
| |
| - (void)moveToBeginningOfDocument:(id)sender { |
| [self handleAction:ui::TextEditCommand::MOVE_TO_BEGINNING_OF_DOCUMENT |
| keyCode:ui::VKEY_HOME |
| domCode:ui::DomCode::HOME |
| eventFlags:ui::EF_CONTROL_DOWN]; |
| } |
| |
| - (void)pageDown:(id)sender { |
| // The pageDown: action message is bound to the key combination |
| // [Option+PageDown]. |
| [self handleAction:ui::TextEditCommand::MOVE_PAGE_DOWN |
| keyCode:ui::VKEY_NEXT |
| domCode:ui::DomCode::PAGE_DOWN |
| eventFlags:ui::EF_ALT_DOWN]; |
| } |
| |
| - (void)pageUp:(id)sender { |
| // The pageUp: action message is bound to the key combination [Option+PageUp]. |
| [self handleAction:ui::TextEditCommand::MOVE_PAGE_UP |
| keyCode:ui::VKEY_PRIOR |
| domCode:ui::DomCode::PAGE_UP |
| eventFlags:ui::EF_ALT_DOWN]; |
| } |
| |
| - (void)moveBackwardAndModifySelection:(id)sender { |
| [self handleAction:ui::TextEditCommand::MOVE_BACKWARD_AND_MODIFY_SELECTION |
| keyCode:ui::VKEY_UNKNOWN |
| domCode:ui::DomCode::NONE |
| eventFlags:0]; |
| } |
| |
| - (void)moveForwardAndModifySelection:(id)sender { |
| [self handleAction:ui::TextEditCommand::MOVE_FORWARD_AND_MODIFY_SELECTION |
| keyCode:ui::VKEY_UNKNOWN |
| domCode:ui::DomCode::NONE |
| eventFlags:0]; |
| } |
| |
| - (void)moveWordForwardAndModifySelection:(id)sender { |
| [self handleAction:ui::TextEditCommand::MOVE_WORD_FORWARD_AND_MODIFY_SELECTION |
| keyCode:ui::VKEY_UNKNOWN |
| domCode:ui::DomCode::NONE |
| eventFlags:0]; |
| } |
| |
| - (void)moveWordBackwardAndModifySelection:(id)sender { |
| [self |
| handleAction:ui::TextEditCommand::MOVE_WORD_BACKWARD_AND_MODIFY_SELECTION |
| keyCode:ui::VKEY_UNKNOWN |
| domCode:ui::DomCode::NONE |
| eventFlags:0]; |
| } |
| |
| - (void)moveUpAndModifySelection:(id)sender { |
| [self handleAction:ui::TextEditCommand::MOVE_UP_AND_MODIFY_SELECTION |
| keyCode:ui::VKEY_UP |
| domCode:ui::DomCode::ARROW_UP |
| eventFlags:ui::EF_SHIFT_DOWN]; |
| } |
| |
| - (void)moveDownAndModifySelection:(id)sender { |
| [self handleAction:ui::TextEditCommand::MOVE_DOWN_AND_MODIFY_SELECTION |
| keyCode:ui::VKEY_DOWN |
| domCode:ui::DomCode::ARROW_DOWN |
| eventFlags:ui::EF_SHIFT_DOWN]; |
| } |
| |
| - (void)moveToBeginningOfLineAndModifySelection:(id)sender { |
| [self handleAction:ui::TextEditCommand:: |
| MOVE_TO_BEGINNING_OF_LINE_AND_MODIFY_SELECTION |
| keyCode:ui::VKEY_HOME |
| domCode:ui::DomCode::HOME |
| eventFlags:ui::EF_SHIFT_DOWN]; |
| } |
| |
| - (void)moveToEndOfLineAndModifySelection:(id)sender { |
| [self |
| handleAction:ui::TextEditCommand::MOVE_TO_END_OF_LINE_AND_MODIFY_SELECTION |
| keyCode:ui::VKEY_END |
| domCode:ui::DomCode::END |
| eventFlags:ui::EF_SHIFT_DOWN]; |
| } |
| |
| - (void)moveToBeginningOfParagraphAndModifySelection:(id)sender { |
| [self handleAction:ui::TextEditCommand:: |
| MOVE_TO_BEGINNING_OF_PARAGRAPH_AND_MODIFY_SELECTION |
| keyCode:ui::VKEY_UNKNOWN |
| domCode:ui::DomCode::NONE |
| eventFlags:0]; |
| } |
| |
| - (void)moveToEndOfParagraphAndModifySelection:(id)sender { |
| [self handleAction:ui::TextEditCommand:: |
| MOVE_TO_END_OF_PARAGRAPH_AND_MODIFY_SELECTION |
| keyCode:ui::VKEY_UNKNOWN |
| domCode:ui::DomCode::NONE |
| eventFlags:0]; |
| } |
| |
| - (void)moveToEndOfDocumentAndModifySelection:(id)sender { |
| [self handleAction:ui::TextEditCommand:: |
| MOVE_TO_END_OF_DOCUMENT_AND_MODIFY_SELECTION |
| keyCode:ui::VKEY_END |
| domCode:ui::DomCode::END |
| eventFlags:ui::EF_CONTROL_DOWN | ui::EF_SHIFT_DOWN]; |
| } |
| |
| - (void)moveToBeginningOfDocumentAndModifySelection:(id)sender { |
| [self handleAction:ui::TextEditCommand:: |
| MOVE_TO_BEGINNING_OF_DOCUMENT_AND_MODIFY_SELECTION |
| keyCode:ui::VKEY_HOME |
| domCode:ui::DomCode::HOME |
| eventFlags:ui::EF_CONTROL_DOWN | ui::EF_SHIFT_DOWN]; |
| } |
| |
| - (void)pageDownAndModifySelection:(id)sender { |
| [self handleAction:ui::TextEditCommand::MOVE_PAGE_DOWN_AND_MODIFY_SELECTION |
| keyCode:ui::VKEY_NEXT |
| domCode:ui::DomCode::PAGE_DOWN |
| eventFlags:ui::EF_SHIFT_DOWN]; |
| } |
| |
| - (void)pageUpAndModifySelection:(id)sender { |
| [self handleAction:ui::TextEditCommand::MOVE_PAGE_UP_AND_MODIFY_SELECTION |
| keyCode:ui::VKEY_PRIOR |
| domCode:ui::DomCode::PAGE_UP |
| eventFlags:ui::EF_SHIFT_DOWN]; |
| } |
| |
| - (void)moveParagraphForwardAndModifySelection:(id)sender { |
| [self handleAction:ui::TextEditCommand:: |
| MOVE_PARAGRAPH_FORWARD_AND_MODIFY_SELECTION |
| keyCode:ui::VKEY_DOWN |
| domCode:ui::DomCode::ARROW_DOWN |
| eventFlags:ui::EF_CONTROL_DOWN | ui::EF_SHIFT_DOWN]; |
| } |
| |
| - (void)moveParagraphBackwardAndModifySelection:(id)sender { |
| [self handleAction:ui::TextEditCommand:: |
| MOVE_PARAGRAPH_BACKWARD_AND_MODIFY_SELECTION |
| keyCode:ui::VKEY_UP |
| domCode:ui::DomCode::ARROW_UP |
| eventFlags:ui::EF_CONTROL_DOWN | ui::EF_SHIFT_DOWN]; |
| } |
| |
| - (void)moveWordRight:(id)sender { |
| [self handleAction:ui::TextEditCommand::MOVE_WORD_RIGHT |
| keyCode:ui::VKEY_RIGHT |
| domCode:ui::DomCode::ARROW_RIGHT |
| eventFlags:ui::EF_CONTROL_DOWN]; |
| } |
| |
| - (void)moveWordLeft:(id)sender { |
| [self handleAction:ui::TextEditCommand::MOVE_WORD_LEFT |
| keyCode:ui::VKEY_LEFT |
| domCode:ui::DomCode::ARROW_LEFT |
| eventFlags:ui::EF_CONTROL_DOWN]; |
| } |
| |
| - (void)moveRightAndModifySelection:(id)sender { |
| [self handleAction:ui::TextEditCommand::MOVE_RIGHT_AND_MODIFY_SELECTION |
| keyCode:ui::VKEY_RIGHT |
| domCode:ui::DomCode::ARROW_RIGHT |
| eventFlags:ui::EF_SHIFT_DOWN]; |
| } |
| |
| - (void)moveLeftAndModifySelection:(id)sender { |
| [self handleAction:ui::TextEditCommand::MOVE_LEFT_AND_MODIFY_SELECTION |
| keyCode:ui::VKEY_LEFT |
| domCode:ui::DomCode::ARROW_LEFT |
| eventFlags:ui::EF_SHIFT_DOWN]; |
| } |
| |
| - (void)moveWordRightAndModifySelection:(id)sender { |
| [self handleAction:ui::TextEditCommand::MOVE_WORD_RIGHT_AND_MODIFY_SELECTION |
| keyCode:ui::VKEY_RIGHT |
| domCode:ui::DomCode::ARROW_RIGHT |
| eventFlags:ui::EF_CONTROL_DOWN | ui::EF_SHIFT_DOWN]; |
| } |
| |
| - (void)moveWordLeftAndModifySelection:(id)sender { |
| [self handleAction:ui::TextEditCommand::MOVE_WORD_LEFT_AND_MODIFY_SELECTION |
| keyCode:ui::VKEY_LEFT |
| domCode:ui::DomCode::ARROW_LEFT |
| eventFlags:ui::EF_CONTROL_DOWN | ui::EF_SHIFT_DOWN]; |
| } |
| |
| - (void)moveToLeftEndOfLine:(id)sender { |
| [self isTextRTL] ? [self moveToEndOfLine:sender] |
| : [self moveToBeginningOfLine:sender]; |
| } |
| |
| - (void)moveToRightEndOfLine:(id)sender { |
| [self isTextRTL] ? [self moveToBeginningOfLine:sender] |
| : [self moveToEndOfLine:sender]; |
| } |
| |
| - (void)moveToLeftEndOfLineAndModifySelection:(id)sender { |
| [self isTextRTL] ? [self moveToEndOfLineAndModifySelection:sender] |
| : [self moveToBeginningOfLineAndModifySelection:sender]; |
| } |
| |
| - (void)moveToRightEndOfLineAndModifySelection:(id)sender { |
| [self isTextRTL] ? [self moveToBeginningOfLineAndModifySelection:sender] |
| : [self moveToEndOfLineAndModifySelection:sender]; |
| } |
| |
| // Graphical Element transposition |
| |
| - (void)transpose:(id)sender { |
| [self handleAction:ui::TextEditCommand::TRANSPOSE |
| keyCode:ui::VKEY_T |
| domCode:ui::DomCode::US_T |
| eventFlags:ui::EF_CONTROL_DOWN]; |
| } |
| |
| // Deletions. |
| |
| - (void)deleteForward:(id)sender { |
| [self handleAction:ui::TextEditCommand::DELETE_FORWARD |
| keyCode:ui::VKEY_DELETE |
| domCode:ui::DomCode::DEL |
| eventFlags:0]; |
| } |
| |
| - (void)deleteBackward:(id)sender { |
| [self handleAction:ui::TextEditCommand::DELETE_BACKWARD |
| keyCode:ui::VKEY_BACK |
| domCode:ui::DomCode::BACKSPACE |
| eventFlags:0]; |
| } |
| |
| - (void)deleteWordForward:(id)sender { |
| [self handleAction:ui::TextEditCommand::DELETE_WORD_FORWARD |
| keyCode:ui::VKEY_DELETE |
| domCode:ui::DomCode::DEL |
| eventFlags:ui::EF_CONTROL_DOWN]; |
| } |
| |
| - (void)deleteWordBackward:(id)sender { |
| [self handleAction:ui::TextEditCommand::DELETE_WORD_BACKWARD |
| keyCode:ui::VKEY_BACK |
| domCode:ui::DomCode::BACKSPACE |
| eventFlags:ui::EF_CONTROL_DOWN]; |
| } |
| |
| - (void)deleteToBeginningOfLine:(id)sender { |
| [self handleAction:ui::TextEditCommand::DELETE_TO_BEGINNING_OF_LINE |
| keyCode:ui::VKEY_BACK |
| domCode:ui::DomCode::BACKSPACE |
| eventFlags:ui::EF_CONTROL_DOWN | ui::EF_SHIFT_DOWN]; |
| } |
| |
| - (void)deleteToEndOfLine:(id)sender { |
| [self handleAction:ui::TextEditCommand::DELETE_TO_END_OF_LINE |
| keyCode:ui::VKEY_DELETE |
| domCode:ui::DomCode::DEL |
| eventFlags:ui::EF_CONTROL_DOWN | ui::EF_SHIFT_DOWN]; |
| } |
| |
| - (void)deleteToBeginningOfParagraph:(id)sender { |
| [self handleAction:ui::TextEditCommand::DELETE_TO_BEGINNING_OF_PARAGRAPH |
| keyCode:ui::VKEY_UNKNOWN |
| domCode:ui::DomCode::NONE |
| eventFlags:0]; |
| } |
| |
| - (void)deleteToEndOfParagraph:(id)sender { |
| [self handleAction:ui::TextEditCommand::DELETE_TO_END_OF_PARAGRAPH |
| keyCode:ui::VKEY_UNKNOWN |
| domCode:ui::DomCode::NONE |
| eventFlags:0]; |
| } |
| |
| - (void)yank:(id)sender { |
| [self handleAction:ui::TextEditCommand::YANK |
| keyCode:ui::VKEY_Y |
| domCode:ui::DomCode::US_Y |
| eventFlags:ui::EF_CONTROL_DOWN]; |
| } |
| |
| // Cancellation. |
| |
| - (void)cancelOperation:(id)sender { |
| [self handleAction:ui::TextEditCommand::INVALID_COMMAND |
| keyCode:ui::VKEY_ESCAPE |
| domCode:ui::DomCode::ESCAPE |
| eventFlags:0]; |
| } |
| |
| // Support for Services in context menus. |
| // Currently we only support reading and writing plain strings. |
| - (id)validRequestorForSendType:(NSString*)sendType |
| returnType:(NSString*)returnType { |
| NSString* const utf8Type = base::mac::CFToNSCast(kUTTypeUTF8PlainText); |
| BOOL canWrite = |
| [sendType isEqualToString:utf8Type] && [self selectedRange].length > 0; |
| BOOL canRead = [returnType isEqualToString:utf8Type]; |
| // Valid if (sendType, returnType) is either (string, nil), (nil, string), |
| // or (string, string). |
| BOOL valid = |
| [self hasTextInputClient] && ((canWrite && (canRead || !returnType)) || |
| (canRead && (canWrite || !sendType))); |
| return valid |
| ? self |
| : [super validRequestorForSendType:sendType returnType:returnType]; |
| } |
| |
| // NSServicesMenuRequestor protocol |
| |
| - (BOOL)writeSelectionToPasteboard:(NSPasteboard*)pboard types:(NSArray*)types { |
| // NB: The NSServicesMenuRequestor protocol has not (as of 10.14) been |
| // upgraded to request UTIs rather than obsolete PboardType constants. Handle |
| // either for when it is upgraded. |
| DCHECK([types containsObject:NSStringPboardType] || |
| [types containsObject:base::mac::CFToNSCast(kUTTypeUTF8PlainText)]); |
| |
| bool result = NO; |
| base::string16 text; |
| if (bridge_) |
| bridge_->text_input_host()->GetSelectionText(&result, &text); |
| if (!result) |
| return NO; |
| return [pboard writeObjects:@[ base::SysUTF16ToNSString(text) ]]; |
| } |
| |
| - (BOOL)readSelectionFromPasteboard:(NSPasteboard*)pboard { |
| NSArray* objects = |
| [pboard readObjectsForClasses:@[ [NSString class] ] options:0]; |
| DCHECK([objects count] == 1); |
| [self insertText:[objects lastObject]]; |
| return YES; |
| } |
| |
| // NSTextInputClient protocol implementation. |
| |
| // IMPORTANT: Always null-check |[self textInputClient]|. It can change (or be |
| // cleared) in -setTextInputClient:, which requires informing AppKit that the |
| // -inputContext has changed and to update its raw pointer. However, the AppKit |
| // method which does that may also spin a nested run loop communicating with an |
| // IME window and cause it to *use* the exact same NSTextInputClient (i.e., |
| // |self|) that we're trying to invalidate in -setTextInputClient:. |
| // See https://crbug.com/817097#c12 for further details on this atrocity. |
| |
| - (NSAttributedString*)attributedSubstringForProposedRange:(NSRange)range |
| actualRange: |
| (NSRangePointer)actualRange { |
| // On TouchBar Macs, the IME subsystem sometimes sends an invalid range with a |
| // non-zero length. This will cause a DCHECK in gfx::Range, so repair it here. |
| // See https://crbug.com/888782. |
| if (range.location == NSNotFound) |
| range.length = 0; |
| base::string16 substring; |
| gfx::Range actual_range = gfx::Range::InvalidRange(); |
| if (bridge_) { |
| bridge_->text_input_host()->GetAttributedSubstringForRange( |
| gfx::Range(range), &substring, &actual_range); |
| } |
| if (actualRange) { |
| // To maintain consistency with NSTextView, return range {0,0} for an out of |
| // bounds requested range. |
| *actualRange = |
| actual_range.IsValid() ? actual_range.ToNSRange() : NSMakeRange(0, 0); |
| } |
| return substring.empty() |
| ? nil |
| : [[[NSAttributedString alloc] |
| initWithString:base::SysUTF16ToNSString(substring)] |
| autorelease]; |
| } |
| |
| - (NSUInteger)characterIndexForPoint:(NSPoint)aPoint { |
| NOTIMPLEMENTED(); |
| return 0; |
| } |
| |
| - (void)doCommandBySelector:(SEL)selector { |
| // Like the renderer, handle insert action messages as a regular key dispatch. |
| // This ensures, e.g., insertTab correctly changes focus between fields. This |
| // handles: |
| // -insertTab:(id)sender |
| // -insertBacktab: |
| // -insertNewline: |
| // -insertParagraphSeparator: |
| // -insertNewlineIgnoringFieldEditor: |
| // -insertTabIgnoringFieldEditor: |
| // -insertLineBreak: |
| // -insertContainerBreak: |
| // -insertSingleQuoteIgnoringSubstitution: |
| // -insertDoubleQuoteIgnoringSubstitution: |
| // It does not handle |-insertText:(id)insertString|, which is not a command. |
| // I.e. AppKit invokes _either_ -insertText: or -doCommandBySelector:. Also |
| // note -insertText: is only invoked if -inputContext: has returned nil. |
| DCHECK_NE(@selector(insertText:), selector); |
| if (keyDownEvent_ && [NSStringFromSelector(selector) hasPrefix:@"insert"]) { |
| // When return is pressed during IME composition, engines typically first |
| // confirm the composition with a series of -insertText:replacementRange: |
| // calls. Then, some also invoke -insertNewLine: (some do not). If an engine |
| // DOES invokes -insertNewLine:, we always want a corresponding VKEY_RETURN |
| // ui::KeyEvent generated. If it does NOT follow with -insertNewLine:, the |
| // VKEY_RETURN must be suppressed in keyDown:, since it typically will have |
| // merely dismissed the IME window: the composition is still ongoing. |
| // Setting this ensures keyDown: always generates a ui::KeyEvent. |
| wantsKeyHandledForInsert_ = YES; |
| return; // Handle in -keyDown:. |
| } |
| |
| if ([self respondsToSelector:selector]) { |
| [self performSelector:selector withObject:nil]; |
| hasUnhandledKeyDownEvent_ = NO; |
| return; |
| } |
| |
| // For events that AppKit sends via doCommandBySelector:, first attempt to |
| // handle as a Widget accelerator. Forward along the responder chain only if |
| // the Widget doesn't handle it. |
| if (![self handleUnhandledKeyDownAsKeyEvent]) |
| [[self nextResponder] doCommandBySelector:selector]; |
| } |
| |
| - (NSRect)firstRectForCharacterRange:(NSRange)range |
| actualRange:(NSRangePointer)actualNSRange { |
| gfx::Rect rect; |
| gfx::Range actualRange = gfx::Range::InvalidRange(); |
| if (bridge_) { |
| bridge_->text_input_host()->GetFirstRectForRange(gfx::Range(range), &rect, |
| &actualRange); |
| } |
| if (actualNSRange) |
| *actualNSRange = actualRange.ToNSRange(); |
| return gfx::ScreenRectToNSRect(rect); |
| } |
| |
| - (BOOL)hasMarkedText { |
| bool hasCompositionText = NO; |
| if (bridge_) |
| bridge_->text_input_host()->HasCompositionText(&hasCompositionText); |
| return hasCompositionText; |
| } |
| |
| - (void)insertText:(id)text replacementRange:(NSRange)replacementRange { |
| if (!bridge_) |
| return; |
| bridge_->text_input_host()->DeleteRange(gfx::Range(replacementRange)); |
| [self insertTextInternal:text]; |
| } |
| |
| - (NSRange)markedRange { |
| gfx::Range range = gfx::Range::InvalidRange(); |
| if (bridge_) |
| bridge_->text_input_host()->GetCompositionTextRange(&range); |
| return range.ToNSRange(); |
| } |
| |
| - (NSRange)selectedRange { |
| gfx::Range range = gfx::Range::InvalidRange(); |
| if (bridge_) |
| bridge_->text_input_host()->GetSelectionRange(&range); |
| return range.ToNSRange(); |
| } |
| |
| - (void)setMarkedText:(id)text |
| selectedRange:(NSRange)selectedRange |
| replacementRange:(NSRange)replacementRange { |
| if (![self hasTextInputClient]) |
| return; |
| |
| if ([text isKindOfClass:[NSAttributedString class]]) |
| text = [text string]; |
| bridge_->text_input_host()->SetCompositionText(base::SysNSStringToUTF16(text), |
| gfx::Range(selectedRange), |
| gfx::Range(replacementRange)); |
| hasUnhandledKeyDownEvent_ = NO; |
| } |
| |
| - (void)unmarkText { |
| if (![self hasTextInputClient]) |
| return; |
| |
| bridge_->text_input_host()->ConfirmCompositionText(); |
| hasUnhandledKeyDownEvent_ = NO; |
| } |
| |
| - (NSArray*)validAttributesForMarkedText { |
| return @[]; |
| } |
| |
| // NSUserInterfaceValidations protocol implementation. |
| |
| - (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)item { |
| ui::TextEditCommand command = GetTextEditCommandForMenuAction([item action]); |
| |
| if (command == ui::TextEditCommand::INVALID_COMMAND) |
| return NO; |
| |
| // TODO(https://crbug.com/901490): Add mojo support for ui::TextEditCommand. |
| if ([self textInputClient]) |
| return [self textInputClient] -> IsTextEditCommandEnabled(command); |
| |
| // views::Label does not implement the TextInputClient interface but still |
| // needs to intercept the Copy and Select All menu actions. |
| if (command != ui::TextEditCommand::COPY && |
| command != ui::TextEditCommand::SELECT_ALL) |
| return NO; |
| |
| bool is_textual = false; |
| bridge_->host()->GetIsFocusedViewTextual(&is_textual); |
| return is_textual; |
| } |
| |
| // NSDraggingSource protocol implementation. |
| |
| - (NSDragOperation)draggingSession:(NSDraggingSession*)session |
| sourceOperationMaskForDraggingContext:(NSDraggingContext)context { |
| return NSDragOperationEvery; |
| } |
| |
| - (void)draggingSession:(NSDraggingSession*)session |
| endedAtPoint:(NSPoint)screenPoint |
| operation:(NSDragOperation)operation { |
| views_bridge_mac::DragDropClient* client = [self dragDropClient]; |
| if (client) |
| client->EndDrag(); |
| } |
| |
| // NSAccessibility informal protocol implementation. |
| |
| - (id)accessibilityAttributeValue:(NSString*)attribute { |
| if ([attribute isEqualToString:NSAccessibilityChildrenAttribute]) { |
| if (id accessible = bridge_->host_helper()->GetNativeViewAccessible()) |
| return @[ accessible ]; |
| } |
| |
| return [super accessibilityAttributeValue:attribute]; |
| } |
| |
| - (id)accessibilityHitTest:(NSPoint)point { |
| return [bridge_->host_helper()->GetNativeViewAccessible() |
| accessibilityHitTest:point]; |
| } |
| |
| - (id)accessibilityFocusedUIElement { |
| // This function should almost-never be called because when |self| is the |
| // first responder for the key NSWindow, BridgedNativeWidgetHostImpl's |
| // AccessibilityFocusOverrider will override the accessibility focus query. |
| if (!bridge_) |
| return nil; |
| return [bridge_->host_helper()->GetNativeViewAccessible() |
| accessibilityFocusedUIElement]; |
| } |
| |
| @end |