1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #import <Cocoa/Cocoa.h>
7 #import "base/mac/scoped_nsobject.h"
8 #import "base/mac/scoped_objc_class_swizzler.h"
9 #include "base/memory/singleton.h"
10 #include "ui/events/event_processor.h"
11 #include "ui/events/event_target.h"
12 #include "ui/events/event_target_iterator.h"
13 #include "ui/events/event_targeter.h"
14 #import "ui/events/test/cocoa_test_event_utils.h"
15 #include "ui/events/test/event_generator.h"
16 #include "ui/gfx/mac/coordinate_conversion.h"
20 // Singleton to provide state for swizzled Objective C methods.
21 ui::test::EventGenerator* g_active_generator = NULL;
23 // Set (and always cleared) in EmulateSendEvent() to provide an answer for
24 // [NSApp currentEvent].
25 NSEvent* g_current_event = nil;
29 @interface NSEventDonor : NSObject
32 @interface NSApplicationDonor : NSObject
37 NSPoint ConvertRootPointToTarget(NSWindow* target,
38 const gfx::Point& point_in_root) {
39 // Normally this would do [NSWindow convertScreenToBase:]. However, Cocoa can
40 // reposition the window on screen and make things flaky. Initially, just
41 // assume that the contentRect of |target| is at the top-left corner of the
43 NSRect content_rect = [target contentRectForFrameRect:[target frame]];
44 return NSMakePoint(point_in_root.x(),
45 NSHeight(content_rect) - point_in_root.y());
48 // Inverse of ui::EventFlagsFromModifiers().
49 NSUInteger EventFlagsToModifiers(int flags) {
50 NSUInteger modifiers = 0;
51 modifiers |= (flags & ui::EF_CAPS_LOCK_DOWN) ? NSAlphaShiftKeyMask : 0;
52 modifiers |= (flags & ui::EF_SHIFT_DOWN) ? NSShiftKeyMask : 0;
53 modifiers |= (flags & ui::EF_CONTROL_DOWN) ? NSControlKeyMask : 0;
54 modifiers |= (flags & ui::EF_ALT_DOWN) ? NSAlternateKeyMask : 0;
55 modifiers |= (flags & ui::EF_COMMAND_DOWN) ? NSCommandKeyMask : 0;
56 // ui::EF_*_MOUSE_BUTTON not handled here.
57 // NSFunctionKeyMask, NSNumericPadKeyMask and NSHelpKeyMask not mapped.
61 // Picks the corresponding mouse event type for the buttons set in |flags|.
62 NSEventType PickMouseEventType(int flags,
66 if (flags & ui::EF_LEFT_MOUSE_BUTTON)
68 if (flags & ui::EF_RIGHT_MOUSE_BUTTON)
73 // Inverse of ui::EventTypeFromNative(). If non-null |modifiers| will be set
74 // using the inverse of ui::EventFlagsFromNSEventWithModifiers().
75 NSEventType EventTypeToNative(ui::EventType ui_event_type,
77 NSUInteger* modifiers) {
79 *modifiers = EventFlagsToModifiers(flags);
80 switch (ui_event_type) {
83 case ui::ET_KEY_PRESSED:
85 case ui::ET_KEY_RELEASED:
87 case ui::ET_MOUSE_PRESSED:
88 return PickMouseEventType(flags,
92 case ui::ET_MOUSE_RELEASED:
93 return PickMouseEventType(flags,
97 case ui::ET_MOUSE_DRAGGED:
98 return PickMouseEventType(flags,
101 NSOtherMouseDragged);
102 case ui::ET_MOUSE_MOVED:
104 case ui::ET_MOUSEWHEEL:
105 return NSScrollWheel;
106 case ui::ET_MOUSE_ENTERED:
107 return NSMouseEntered;
108 case ui::ET_MOUSE_EXITED:
109 return NSMouseExited;
110 case ui::ET_SCROLL_FLING_START:
111 return NSEventTypeSwipe;
118 // Emulate the dispatching that would be performed by -[NSWindow sendEvent:].
119 // sendEvent is a black box which (among other things) will try to peek at the
120 // event queue and can block indefinitely.
121 void EmulateSendEvent(NSWindow* window, NSEvent* event) {
122 base::AutoReset<NSEvent*> reset(&g_current_event, event);
123 NSResponder* responder = [window firstResponder];
124 switch ([event type]) {
126 [responder keyDown:event];
129 [responder keyUp:event];
133 // For mouse events, NSWindow will use -[NSView hitTest:] for the initial
134 // mouseDown, and then keep track of the NSView returned. The toolkit-views
135 // RootView does this too. So, for tests, assume tracking will be done there,
136 // and the NSWindow's contentView is wrapping a views::internal::RootView.
137 responder = [window contentView];
138 switch ([event type]) {
139 case NSLeftMouseDown:
140 [responder mouseDown:event];
142 case NSRightMouseDown:
143 [responder rightMouseDown:event];
145 case NSOtherMouseDown:
146 [responder otherMouseDown:event];
149 [responder mouseUp:event];
152 [responder rightMouseUp:event];
155 [responder otherMouseUp:event];
157 case NSLeftMouseDragged:
158 [responder mouseDragged:event];
160 case NSRightMouseDragged:
161 [responder rightMouseDragged:event];
163 case NSOtherMouseDragged:
164 [responder otherMouseDragged:event];
167 // Assumes [NSWindow acceptsMouseMovedEvents] would return YES, and that
168 // NSTrackingAreas have been appropriately installed on |responder|.
169 [responder mouseMoved:event];
172 [responder scrollWheel:event];
176 // With the assumptions in NSMouseMoved, it doesn't make sense for the
177 // generator to handle entered/exited separately. It's the responsibility
178 // of views::internal::RootView to convert the moved events into entered
179 // and exited events for the individual views.
182 case NSEventTypeSwipe:
183 // NSEventTypeSwipe events can't be generated using public interfaces on
184 // NSEvent, so this will need to be handled at a higher level.
192 NSEvent* CreateMouseEventInWindow(NSWindow* window,
193 ui::EventType event_type,
194 const gfx::Point& point_in_root,
196 NSUInteger click_count = 0;
197 if (event_type == ui::ET_MOUSE_PRESSED ||
198 event_type == ui::ET_MOUSE_RELEASED) {
199 if (flags & ui::EF_IS_TRIPLE_CLICK)
201 else if (flags & ui::EF_IS_DOUBLE_CLICK)
206 NSPoint point = ConvertRootPointToTarget(window, point_in_root);
207 NSUInteger modifiers = 0;
208 NSEventType type = EventTypeToNative(event_type, flags, &modifiers);
209 return [NSEvent mouseEventWithType:type
211 modifierFlags:modifiers
213 windowNumber:[window windowNumber]
216 clickCount:click_count
220 // Implementation of ui::test::EventGeneratorDelegate for Mac. Everything
221 // defined inline is just a stub. Interesting overrides are defined below the
223 class EventGeneratorDelegateMac : public ui::EventTarget,
224 public ui::EventSource,
225 public ui::EventProcessor,
226 public ui::EventTargeter,
227 public ui::test::EventGeneratorDelegate {
229 static EventGeneratorDelegateMac* GetInstance() {
230 return Singleton<EventGeneratorDelegateMac>::get();
233 IMP CurrentEventMethod() {
234 return swizzle_current_event_->GetOriginalImplementation();
237 // Overridden from ui::EventTarget:
238 bool CanAcceptEvent(const ui::Event& event) override { return true; }
239 ui::EventTarget* GetParentTarget() override { return NULL; }
240 scoped_ptr<ui::EventTargetIterator> GetChildIterator() const override;
241 ui::EventTargeter* GetEventTargeter() override { return this; }
243 // Overridden from ui::EventHandler (via ui::EventTarget):
244 void OnMouseEvent(ui::MouseEvent* event) override;
245 void OnKeyEvent(ui::KeyEvent* event) override;
247 // Overridden from ui::EventSource:
248 ui::EventProcessor* GetEventProcessor() override { return this; }
250 // Overridden from ui::EventProcessor:
251 ui::EventTarget* GetRootTarget() override { return this; }
253 // Overridden from ui::EventDispatcherDelegate (via ui::EventProcessor):
254 bool CanDispatchToTarget(EventTarget* target) override { return true; }
256 // Overridden from ui::test::EventGeneratorDelegate:
257 void SetContext(ui::test::EventGenerator* owner,
258 gfx::NativeWindow root_window,
259 gfx::NativeWindow window) override;
260 ui::EventTarget* GetTargetAt(const gfx::Point& location) override {
263 ui::EventSource* GetEventSource(ui::EventTarget* target) override {
266 gfx::Point CenterOfTarget(const ui::EventTarget* target) const override;
267 gfx::Point CenterOfWindow(gfx::NativeWindow window) const override;
269 void ConvertPointFromTarget(const ui::EventTarget* target,
270 gfx::Point* point) const override {}
271 void ConvertPointToTarget(const ui::EventTarget* target,
272 gfx::Point* point) const override {}
273 void ConvertPointFromHost(const ui::EventTarget* hosted_target,
274 gfx::Point* point) const override {}
277 friend struct DefaultSingletonTraits<EventGeneratorDelegateMac>;
279 EventGeneratorDelegateMac();
280 ~EventGeneratorDelegateMac() override;
282 ui::test::EventGenerator* owner_;
284 scoped_ptr<base::mac::ScopedObjCClassSwizzler> swizzle_pressed_;
285 scoped_ptr<base::mac::ScopedObjCClassSwizzler> swizzle_current_event_;
286 base::scoped_nsobject<NSMenu> fake_menu_;
288 DISALLOW_COPY_AND_ASSIGN(EventGeneratorDelegateMac);
291 EventGeneratorDelegateMac::EventGeneratorDelegateMac()
294 DCHECK(!ui::test::EventGenerator::default_delegate);
295 ui::test::EventGenerator::default_delegate = this;
296 // Install a fake "edit" menu. This is normally provided by Chrome's
297 // MainMenu.xib, but src/ui shouldn't depend on that.
298 fake_menu_.reset([[NSMenu alloc] initWithTitle:@"Edit"]);
302 NSString* key_equivalent;
303 } fake_menu_item[] = {
304 {@"Undo", @selector(undo:), @"z"},
305 {@"Redo", @selector(redo:), @"Z"},
306 {@"Copy", @selector(copy:), @"c"},
307 {@"Cut", @selector(cut:), @"x"},
308 {@"Paste", @selector(paste:), @"v"},
309 {@"Select All", @selector(selectAll:), @"a"},
311 for (size_t i = 0; i < arraysize(fake_menu_item); ++i) {
312 [fake_menu_ insertItemWithTitle:fake_menu_item[i].title
313 action:fake_menu_item[i].action
314 keyEquivalent:fake_menu_item[i].key_equivalent
319 EventGeneratorDelegateMac::~EventGeneratorDelegateMac() {
320 DCHECK_EQ(this, ui::test::EventGenerator::default_delegate);
321 ui::test::EventGenerator::default_delegate = NULL;
324 scoped_ptr<ui::EventTargetIterator>
325 EventGeneratorDelegateMac::GetChildIterator() const {
326 // Return NULL to dispatch all events to the result of GetRootTarget().
330 void EventGeneratorDelegateMac::OnMouseEvent(ui::MouseEvent* event) {
331 // For mouse drag events, ensure the swizzled methods return the right flags.
332 base::AutoReset<ui::test::EventGenerator*> reset(&g_active_generator, owner_);
333 NSEvent* ns_event = CreateMouseEventInWindow(window_,
336 event->changed_button_flags());
337 if (owner_->targeting_application())
338 [NSApp sendEvent:ns_event];
340 EmulateSendEvent(window_, ns_event);
343 void EventGeneratorDelegateMac::OnKeyEvent(ui::KeyEvent* event) {
344 NSUInteger modifiers = EventFlagsToModifiers(event->flags());
345 NSEvent* ns_event = cocoa_test_event_utils::SynthesizeKeyEvent(
346 window_, event->type() == ui::ET_KEY_PRESSED, event->key_code(),
348 if (owner_->targeting_application()) {
349 [NSApp sendEvent:ns_event];
353 if ([fake_menu_ performKeyEquivalent:ns_event])
356 EmulateSendEvent(window_, ns_event);
359 void EventGeneratorDelegateMac::SetContext(ui::test::EventGenerator* owner,
360 gfx::NativeWindow root_window,
361 gfx::NativeWindow window) {
362 swizzle_pressed_.reset();
363 swizzle_current_event_.reset();
367 // Normally, edit menu items have a `nil` target. This results in -[NSMenu
368 // performKeyEquivalent:] relying on -[NSApplication targetForAction:to:from:]
369 // to find a target starting at the first responder of the key window. Since
370 // non-interactive tests have no key window, that won't work. So set (or
371 // clear) the target explicitly on all menu items.
372 [[fake_menu_ itemArray] makeObjectsPerformSelector:@selector(setTarget:)
373 withObject:[window firstResponder]];
376 swizzle_pressed_.reset(new base::mac::ScopedObjCClassSwizzler(
378 [NSEventDonor class],
379 @selector(pressedMouseButtons)));
380 swizzle_current_event_.reset(new base::mac::ScopedObjCClassSwizzler(
381 [NSApplication class],
382 [NSApplicationDonor class],
383 @selector(currentEvent)));
387 gfx::Point EventGeneratorDelegateMac::CenterOfTarget(
388 const ui::EventTarget* target) const {
389 DCHECK_EQ(target, this);
390 return CenterOfWindow(window_);
393 gfx::Point EventGeneratorDelegateMac::CenterOfWindow(
394 gfx::NativeWindow window) const {
395 DCHECK_EQ(window, window_);
396 return gfx::ScreenRectFromNSRect([window frame]).CenterPoint();
404 void InitializeMacEventGeneratorDelegate() {
405 EventGeneratorDelegateMac::GetInstance();
411 @implementation NSEventDonor
413 // Donate +[NSEvent pressedMouseButtons] by retrieving the flags from the
415 + (NSUInteger)pressedMouseButtons {
416 if (!g_active_generator)
417 return [NSEventDonor pressedMouseButtons]; // Call original implementation.
419 int flags = g_active_generator->flags();
420 NSUInteger bitmask = 0;
421 if (flags & ui::EF_LEFT_MOUSE_BUTTON)
423 if (flags & ui::EF_RIGHT_MOUSE_BUTTON)
425 if (flags & ui::EF_MIDDLE_MOUSE_BUTTON)
432 @implementation NSApplicationDonor
434 - (NSEvent*)currentEvent {
436 return g_current_event;
438 // Find the original implementation and invoke it.
439 IMP original = EventGeneratorDelegateMac::GetInstance()->CurrentEventMethod();
440 return original(self, _cmd);