summaryrefslogtreecommitdiff
path: root/chromium/ui/base/cocoa/command_dispatcher.mm
blob: c905b6be5a5b53ed7e5143d5e1368dbc08dc3db4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
// Copyright 2015 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/base/cocoa/command_dispatcher.h"

#include "base/auto_reset.h"
#include "base/check_op.h"
#include "base/notreached.h"
#include "base/trace_event/trace_event.h"
#include "ui/base/cocoa/cocoa_base_utils.h"
#import "ui/base/cocoa/user_interface_item_command_handler.h"

// Expose -[NSWindow hasKeyAppearance], which determines whether the traffic
// lights on the window are "lit". CommandDispatcher uses this property on a
// parent window to decide whether keys and commands should bubble up.
@interface NSWindow (PrivateAPI)
- (BOOL)hasKeyAppearance;
@end

@interface CommandDispatcher ()
// The parent to bubble events to, or nil.
- (NSWindow<CommandDispatchingWindow>*)bubbleParent;
@end

namespace {

// Duplicate the given key event, but changing the associated window.
NSEvent* KeyEventForWindow(NSWindow* window, NSEvent* event) {
  NSEventType event_type = [event type];

  // Convert the event's location from the original window's coordinates into
  // our own.
  NSPoint location = [event locationInWindow];
  location = ui::ConvertPointFromWindowToScreen([event window], location);
  location = ui::ConvertPointFromScreenToWindow(window, location);

  // Various things *only* apply to key down/up.
  bool is_a_repeat = false;
  NSString* characters = nil;
  NSString* charactors_ignoring_modifiers = nil;
  if (event_type == NSKeyDown || event_type == NSKeyUp) {
    is_a_repeat = [event isARepeat];
    characters = [event characters];
    charactors_ignoring_modifiers = [event charactersIgnoringModifiers];
  }

  // This synthesis may be slightly imperfect: we provide nil for the context,
  // since I (viettrungluu) am sceptical that putting in the original context
  // (if one is given) is valid.
  return [NSEvent keyEventWithType:event_type
                          location:location
                     modifierFlags:[event modifierFlags]
                         timestamp:[event timestamp]
                      windowNumber:[window windowNumber]
                           context:nil
                        characters:characters
       charactersIgnoringModifiers:charactors_ignoring_modifiers
                         isARepeat:is_a_repeat
                           keyCode:[event keyCode]];
}

}  // namespace

@implementation CommandDispatcher {
 @private
  BOOL _eventHandled;
  BOOL _isRedispatchingKeyEvent;

  NSWindow<CommandDispatchingWindow>* _owner;  // Weak, owns us.
}

@synthesize delegate = _delegate;

- (instancetype)initWithOwner:(NSWindow<CommandDispatchingWindow>*)owner {
  if ((self = [super init])) {
    _owner = owner;
  }
  return self;
}

// When an event is being redispatched, its window is rewritten to be the owner_
// of the CommandDispatcher. However, AppKit may still choose to send the event
// to the key window. To check of an event is being redispatched, we check the
// event's window.
- (BOOL)isEventBeingRedispatched:(NSEvent*)event {
  if ([event.window conformsToProtocol:@protocol(CommandDispatchingWindow)]) {
    NSObject<CommandDispatchingWindow>* window =
        static_cast<NSObject<CommandDispatchingWindow>*>(event.window);
    return [window commandDispatcher]->_isRedispatchingKeyEvent;
  }
  return NO;
}

// |delegate_| may be nil in this method. Rather than adding nil checks to every
// call, we rely on the fact that method calls to nil return nil, and that nil
// == ui::PerformKeyEquivalentResult::kUnhandled;
- (BOOL)performKeyEquivalent:(NSEvent*)event {
  // TODO(bokan): Tracing added temporarily to diagnose crbug.com/1039833.
  TRACE_EVENT2("ui", "CommandDispatcher::performKeyEquivalent", "window num",
               [_owner windowNumber], "is keyWin", [NSApp keyWindow] == _owner);
  DCHECK_EQ(NSKeyDown, [event type]);

  // If the event is being redispatched, then this is the second time
  // performKeyEquivalent: is being called on the event. The first time, a
  // WebContents was firstResponder and claimed to have handled the event [but
  // instead sent the event asynchronously to the renderer process]. The
  // renderer process chose not to handle the event, and the consumer
  // redispatched the event by calling -[CommandDispatchingWindow
  // redispatchKeyEvent:].
  //
  // We skip all steps before postPerformKeyEquivalent, since those were already
  // triggered on the first pass of the event.
  if ([self isEventBeingRedispatched:event]) {
    // TODO(bokan): Tracing added temporarily to diagnose crbug.com/1039833.
    TRACE_EVENT_INSTANT0("ui", "IsRedispatch", TRACE_EVENT_SCOPE_THREAD);
    ui::PerformKeyEquivalentResult result =
        [_delegate postPerformKeyEquivalent:event
                                     window:_owner
                               isRedispatch:YES];
    TRACE_EVENT_INSTANT1("ui", "postPerformKeyEquivalent",
                         TRACE_EVENT_SCOPE_THREAD, "result", result);
    if (result == ui::PerformKeyEquivalentResult::kHandled)
      return YES;
    if (result == ui::PerformKeyEquivalentResult::kPassToMainMenu)
      return NO;
    return [[self bubbleParent] performKeyEquivalent:event];
  }

  // First, give the delegate an opportunity to consume this event.
  ui::PerformKeyEquivalentResult result =
      [_delegate prePerformKeyEquivalent:event window:_owner];
  if (result == ui::PerformKeyEquivalentResult::kHandled ||
      result == ui::PerformKeyEquivalentResult::kDrop)
    return YES;
  if (result == ui::PerformKeyEquivalentResult::kPassToMainMenu)
    return NO;

  // Next, pass the event down the NSView hierarchy. Surprisingly, this doesn't
  // use the responder chain. See implementation of -[NSWindow
  // performKeyEquivalent:]. If the view hierarchy contains a
  // RenderWidgetHostViewCocoa, it may choose to return true, and to
  // asynchronously pass the event to the renderer. See
  // -[RenderWidgetHostViewCocoa performKeyEquivalent:].
  if ([_owner defaultPerformKeyEquivalent:event])
    return YES;

  // If the firstResponder [e.g. omnibox] chose not to handle the keyEquivalent,
  // then give the delegate another chance to consume it.
  result =
      [_delegate postPerformKeyEquivalent:event window:_owner isRedispatch:NO];
  if (result == ui::PerformKeyEquivalentResult::kHandled)
    return YES;
  if (result == ui::PerformKeyEquivalentResult::kPassToMainMenu)
    return NO;

  // Allow commands to "bubble up" to CommandDispatchers in parent windows, if
  // they were not handled here.
  return [[self bubbleParent] performKeyEquivalent:event];
}

- (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)item
                       forHandler:(id<UserInterfaceItemCommandHandler>)handler {
  // Since this class implements these selectors, |super| will always say they
  // are enabled. Only use [super] to validate other selectors. If there is no
  // command handler, defer to AppController.
  if ([item action] == @selector(commandDispatch:) ||
      [item action] == @selector(commandDispatchUsingKeyModifiers:)) {
    if (handler) {
      // -dispatch:.. can't later decide to bubble events because
      // -commandDispatch:.. is assumed to always succeed. So, if there is a
      // |handler|, only validate against that for -commandDispatch:.
      return [handler validateUserInterfaceItem:item window:_owner];
    }

    id appController = [NSApp delegate];
    DCHECK([appController
        conformsToProtocol:@protocol(NSUserInterfaceValidations)]);
    if ([appController validateUserInterfaceItem:item])
      return YES;
  }

  // Note this may validate an action bubbled up from a child window. However,
  // if the child window also -respondsToSelector: (but validated it `NO`), the
  // action will be dispatched to the child only, which may NSBeep().
  // TODO(tapted): Fix this. E.g. bubble up validation via the bubbleParent's
  // CommandDispatcher rather than the NSUserInterfaceValidations protocol, so
  // that this step can be skipped.
  if ([_owner defaultValidateUserInterfaceItem:item])
    return YES;

  return [[self bubbleParent] validateUserInterfaceItem:item];
}

- (BOOL)redispatchKeyEvent:(NSEvent*)event {
  // TODO(bokan): Tracing added temporarily to diagnose crbug.com/1039833.
  TRACE_EVENT2("ui", "CommandDispatcher::redispatchKeyEvent", "window num",
               [_owner windowNumber], "event window num",
               [[event window] windowNumber]);
  DCHECK(!_isRedispatchingKeyEvent);
  base::AutoReset<BOOL> resetter(&_isRedispatchingKeyEvent, YES);

  DCHECK(event);
  NSEventType eventType = [event type];
  if (eventType != NSKeyDown && eventType != NSKeyUp &&
      eventType != NSFlagsChanged) {
    NOTREACHED();
    return YES;  // Pretend it's been handled in an effort to limit damage.
  }

  // Sometimes, an event will be redispatched from a child window to a parent
  // window to allow the parent window a chance to handle it. In that case, fix
  // up the native event to reference the correct window. Failure to do this can
  // cause infinite redispatch loops; see https://crbug.com/1085578 for more
  // details.
  if ([event window] != _owner)
    event = KeyEventForWindow(_owner, event);

  // Redispatch the event.
  _eventHandled = YES;
  [NSApp sendEvent:event];

  // If the event was not handled by [NSApp sendEvent:], the preSendEvent:
  // method below will be called, and because the event is being redispatched,
  // |eventHandled_| will be set to NO.
  return _eventHandled;
}

- (BOOL)preSendEvent:(NSEvent*)event {
  // TODO(bokan): Tracing added temporarily to diagnose crbug.com/1039833.
  TRACE_EVENT2("ui", "CommandDispatcher::preSendEvent", "window num",
               [_owner windowNumber], "event window num",
               [[event window] windowNumber]);

  // AppKit does not call performKeyEquivalent: if the event only has the
  // NSEventModifierFlagOption modifier. However, Chrome wants to treat these
  // events just like keyEquivalents, since they can be consumed by extensions.
  if ([event type] == NSKeyDown &&
      ([event modifierFlags] & NSEventModifierFlagOption)) {
    BOOL handled = [self performKeyEquivalent:event];
    if (handled)
      return YES;
  }

  if ([self isEventBeingRedispatched:event]) {
    // If we get here, then the event was not handled by NSApplication.
    _eventHandled = NO;
    // Return YES to stop native -sendEvent handling.
    return YES;
  }

  return NO;
}

- (void)dispatch:(id)sender
      forHandler:(id<UserInterfaceItemCommandHandler>)handler {
  if (handler)
    [handler commandDispatch:sender window:_owner];
  else
    [[self bubbleParent] commandDispatch:sender];
}

- (void)dispatchUsingKeyModifiers:(id)sender
                       forHandler:(id<UserInterfaceItemCommandHandler>)handler {
  if (handler)
    [handler commandDispatchUsingKeyModifiers:sender window:_owner];
  else
    [[self bubbleParent] commandDispatchUsingKeyModifiers:sender];
}

- (NSWindow<CommandDispatchingWindow>*)bubbleParent {
  NSWindow* parent = [_owner parentWindow];
  if (parent && [parent hasKeyAppearance] &&
      [parent conformsToProtocol:@protocol(CommandDispatchingWindow)])
    return static_cast<NSWindow<CommandDispatchingWindow>*>(parent);
  return nil;
}

@end