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 // Set (and always cleared) in EmulateSendEvent() to provide an answer for
21 // [NSApp currentEvent].
22 NSEvent* g_current_event = nil;
26 @interface NSEventDonor : NSObject
29 @interface NSApplicationDonor : NSObject
34 NSPoint ConvertRootPointToTarget(NSWindow* target,
35 const gfx::Point& point_in_root) {
36 // Normally this would do [NSWindow convertScreenToBase:]. However, Cocoa can
37 // reposition the window on screen and make things flaky. Initially, just
38 // assume that the contentRect of |target| is at the top-left corner of the
40 NSRect content_rect = [target contentRectForFrameRect:[target frame]];
41 return NSMakePoint(point_in_root.x(),
42 NSHeight(content_rect) - point_in_root.y());
45 // Inverse of ui::EventFlagsFromModifiers().
46 NSUInteger EventFlagsToModifiers(int flags) {
47 NSUInteger modifiers = 0;
48 modifiers |= (flags & ui::EF_CAPS_LOCK_DOWN) ? NSAlphaShiftKeyMask : 0;
49 modifiers |= (flags & ui::EF_SHIFT_DOWN) ? NSShiftKeyMask : 0;
50 modifiers |= (flags & ui::EF_CONTROL_DOWN) ? NSControlKeyMask : 0;
51 modifiers |= (flags & ui::EF_ALT_DOWN) ? NSAlternateKeyMask : 0;
52 modifiers |= (flags & ui::EF_COMMAND_DOWN) ? NSCommandKeyMask : 0;
53 // ui::EF_*_MOUSE_BUTTON not handled here.
54 // NSFunctionKeyMask, NSNumericPadKeyMask and NSHelpKeyMask not mapped.
58 // Picks the corresponding mouse event type for the buttons set in |flags|.
59 NSEventType PickMouseEventType(int flags,
63 if (flags & ui::EF_LEFT_MOUSE_BUTTON)
65 if (flags & ui::EF_RIGHT_MOUSE_BUTTON)
70 // Inverse of ui::EventTypeFromNative(). If non-null |modifiers| will be set
71 // using the inverse of ui::EventFlagsFromNSEventWithModifiers().
72 NSEventType EventTypeToNative(ui::EventType ui_event_type,
74 NSUInteger* modifiers) {
76 *modifiers = EventFlagsToModifiers(flags);
77 switch (ui_event_type) {
78 case ui::ET_KEY_PRESSED:
80 case ui::ET_KEY_RELEASED:
82 case ui::ET_MOUSE_PRESSED:
83 return PickMouseEventType(flags,
87 case ui::ET_MOUSE_RELEASED:
88 return PickMouseEventType(flags,
92 case ui::ET_MOUSE_DRAGGED:
93 return PickMouseEventType(flags,
97 case ui::ET_MOUSE_MOVED:
99 case ui::ET_MOUSEWHEEL:
100 return NSScrollWheel;
101 case ui::ET_MOUSE_ENTERED:
102 return NSMouseEntered;
103 case ui::ET_MOUSE_EXITED:
104 return NSMouseExited;
105 case ui::ET_SCROLL_FLING_START:
106 return NSEventTypeSwipe;
109 return NSApplicationDefined;
113 // Emulate the dispatching that would be performed by -[NSWindow sendEvent:].
114 // sendEvent is a black box which (among other things) will try to peek at the
115 // event queue and can block indefinitely.
116 void EmulateSendEvent(NSWindow* window, NSEvent* event) {
117 base::AutoReset<NSEvent*> reset(&g_current_event, event);
118 NSResponder* responder = [window firstResponder];
119 switch ([event type]) {
121 [responder keyDown:event];
124 [responder keyUp:event];
130 // For mouse events, NSWindow will use -[NSView hitTest:] for the initial
131 // mouseDown, and then keep track of the NSView returned. The toolkit-views
132 // RootView does this too. So, for tests, assume tracking will be done there,
133 // and the NSWindow's contentView is wrapping a views::internal::RootView.
134 responder = [window contentView];
135 switch ([event type]) {
136 case NSLeftMouseDown:
137 [responder mouseDown:event];
139 case NSRightMouseDown:
140 [responder rightMouseDown:event];
142 case NSOtherMouseDown:
143 [responder otherMouseDown:event];
146 [responder mouseUp:event];
149 [responder rightMouseUp:event];
152 [responder otherMouseUp:event];
154 case NSLeftMouseDragged:
155 [responder mouseDragged:event];
157 case NSRightMouseDragged:
158 [responder rightMouseDragged:event];
160 case NSOtherMouseDragged:
161 [responder otherMouseDragged:event];
164 // Assumes [NSWindow acceptsMouseMovedEvents] would return YES, and that
165 // NSTrackingAreas have been appropriately installed on |responder|.
166 [responder mouseMoved:event];
169 [responder scrollWheel:event];
173 // With the assumptions in NSMouseMoved, it doesn't make sense for the
174 // generator to handle entered/exited separately. It's the responsibility
175 // of views::internal::RootView to convert the moved events into entered
176 // and exited events for the individual views.
179 case NSEventTypeSwipe:
180 // NSEventTypeSwipe events can't be generated using public interfaces on
181 // NSEvent, so this will need to be handled at a higher level.
189 NSEvent* CreateMouseEventInWindow(NSWindow* window,
190 ui::EventType event_type,
191 const gfx::Point& point_in_root,
193 NSUInteger click_count = 0;
194 if (event_type == ui::ET_MOUSE_PRESSED ||
195 event_type == ui::ET_MOUSE_RELEASED) {
196 if (flags & ui::EF_IS_TRIPLE_CLICK)
198 else if (flags & ui::EF_IS_DOUBLE_CLICK)
203 NSPoint point = ConvertRootPointToTarget(window, point_in_root);
204 NSUInteger modifiers = 0;
205 NSEventType type = EventTypeToNative(event_type, flags, &modifiers);
206 return [NSEvent mouseEventWithType:type
208 modifierFlags:modifiers
210 windowNumber:[window windowNumber]
213 clickCount:click_count
217 // Implementation of ui::test::EventGeneratorDelegate for Mac. Everything
218 // defined inline is just a stub. Interesting overrides are defined below the
220 class EventGeneratorDelegateMac : public ui::EventTarget,
221 public ui::EventSource,
222 public ui::EventProcessor,
223 public ui::EventTargeter,
224 public ui::test::EventGeneratorDelegate {
226 static EventGeneratorDelegateMac* GetInstance() {
227 return Singleton<EventGeneratorDelegateMac>::get();
230 IMP CurrentEventMethod() {
231 return swizzle_current_event_->GetOriginalImplementation();
234 NSWindow* window() { return window_.get(); }
235 ui::test::EventGenerator* owner() { return owner_; }
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;
246 void OnTouchEvent(ui::TouchEvent* event) override;
248 // Overridden from ui::EventSource:
249 ui::EventProcessor* GetEventProcessor() override { return this; }
251 // Overridden from ui::EventProcessor:
252 ui::EventTarget* GetRootTarget() override { return this; }
254 // Overridden from ui::EventDispatcherDelegate (via ui::EventProcessor):
255 bool CanDispatchToTarget(EventTarget* target) override { return true; }
257 // Overridden from ui::test::EventGeneratorDelegate:
258 void SetContext(ui::test::EventGenerator* owner,
259 gfx::NativeWindow root_window,
260 gfx::NativeWindow window) override;
261 ui::EventTarget* GetTargetAt(const gfx::Point& location) override {
264 ui::EventSource* GetEventSource(ui::EventTarget* target) override {
267 gfx::Point CenterOfTarget(const ui::EventTarget* target) const override;
268 gfx::Point CenterOfWindow(gfx::NativeWindow window) const override;
270 void ConvertPointFromTarget(const ui::EventTarget* target,
271 gfx::Point* point) const override {}
272 void ConvertPointToTarget(const ui::EventTarget* target,
273 gfx::Point* point) const override {}
274 void ConvertPointFromHost(const ui::EventTarget* hosted_target,
275 gfx::Point* point) const override {}
278 friend struct DefaultSingletonTraits<EventGeneratorDelegateMac>;
280 EventGeneratorDelegateMac();
281 ~EventGeneratorDelegateMac() override;
283 ui::test::EventGenerator* owner_;
284 base::scoped_nsobject<NSWindow> window_;
285 scoped_ptr<base::mac::ScopedObjCClassSwizzler> swizzle_pressed_;
286 scoped_ptr<base::mac::ScopedObjCClassSwizzler> swizzle_location_;
287 scoped_ptr<base::mac::ScopedObjCClassSwizzler> swizzle_current_event_;
288 base::scoped_nsobject<NSMenu> fake_menu_;
290 DISALLOW_COPY_AND_ASSIGN(EventGeneratorDelegateMac);
293 EventGeneratorDelegateMac::EventGeneratorDelegateMac() : owner_(NULL) {
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 NSEvent* ns_event = CreateMouseEventInWindow(window_,
334 event->changed_button_flags());
335 if (owner_->targeting_application())
336 [NSApp sendEvent:ns_event];
338 EmulateSendEvent(window_, ns_event);
341 void EventGeneratorDelegateMac::OnKeyEvent(ui::KeyEvent* event) {
342 NSUInteger modifiers = EventFlagsToModifiers(event->flags());
343 NSEvent* ns_event = cocoa_test_event_utils::SynthesizeKeyEvent(
344 window_, event->type() == ui::ET_KEY_PRESSED, event->key_code(),
346 if (owner_->targeting_application()) {
347 [NSApp sendEvent:ns_event];
351 if ([fake_menu_ performKeyEquivalent:ns_event])
354 EmulateSendEvent(window_, ns_event);
357 void EventGeneratorDelegateMac::OnTouchEvent(ui::TouchEvent* event) {
358 NOTREACHED() << "Touchscreen events not supported on Chrome Mac.";
361 void EventGeneratorDelegateMac::SetContext(ui::test::EventGenerator* owner,
362 gfx::NativeWindow root_window,
363 gfx::NativeWindow window) {
364 swizzle_pressed_.reset();
365 swizzle_location_.reset();
366 swizzle_current_event_.reset();
369 // Retain the NSWindow (note it can be nil). This matches Cocoa's tendency to
370 // have autoreleased objects, or objects still in the event queue, that
371 // reference the NSWindow.
372 window_.reset([window retain]);
374 // Normally, edit menu items have a `nil` target. This results in -[NSMenu
375 // performKeyEquivalent:] relying on -[NSApplication targetForAction:to:from:]
376 // to find a target starting at the first responder of the key window. Since
377 // non-interactive tests have no key window, that won't work. So set (or
378 // clear) the target explicitly on all menu items.
379 [[fake_menu_ itemArray] makeObjectsPerformSelector:@selector(setTarget:)
380 withObject:[window firstResponder]];
383 swizzle_pressed_.reset(new base::mac::ScopedObjCClassSwizzler(
385 [NSEventDonor class],
386 @selector(pressedMouseButtons)));
387 swizzle_location_.reset(new base::mac::ScopedObjCClassSwizzler(
388 [NSEvent class], [NSEventDonor class], @selector(mouseLocation)));
389 swizzle_current_event_.reset(new base::mac::ScopedObjCClassSwizzler(
390 [NSApplication class],
391 [NSApplicationDonor class],
392 @selector(currentEvent)));
396 gfx::Point EventGeneratorDelegateMac::CenterOfTarget(
397 const ui::EventTarget* target) const {
398 DCHECK_EQ(target, this);
399 return CenterOfWindow(window_);
402 gfx::Point EventGeneratorDelegateMac::CenterOfWindow(
403 gfx::NativeWindow window) const {
404 DCHECK_EQ(window, window_);
405 return gfx::ScreenRectFromNSRect([window frame]).CenterPoint();
408 // Return the current owner of the EventGeneratorDelegate. May be null.
409 ui::test::EventGenerator* GetActiveGenerator() {
410 return EventGeneratorDelegateMac::GetInstance()->owner();
418 void InitializeMacEventGeneratorDelegate() {
419 EventGeneratorDelegateMac::GetInstance();
425 @implementation NSEventDonor
427 // Donate +[NSEvent pressedMouseButtons] by retrieving the flags from the
429 + (NSUInteger)pressedMouseButtons {
430 ui::test::EventGenerator* generator = GetActiveGenerator();
432 return [NSEventDonor pressedMouseButtons]; // Call original implementation.
434 int flags = generator->flags();
435 NSUInteger bitmask = 0;
436 if (flags & ui::EF_LEFT_MOUSE_BUTTON)
438 if (flags & ui::EF_RIGHT_MOUSE_BUTTON)
440 if (flags & ui::EF_MIDDLE_MOUSE_BUTTON)
445 // Donate +[NSEvent mouseLocation] by retrieving the current position on screen.
446 + (NSPoint)mouseLocation {
447 ui::test::EventGenerator* generator = GetActiveGenerator();
449 return [NSEventDonor mouseLocation]; // Call original implementation.
451 // The location is the point in the root window which, for desktop widgets, is
452 // the widget itself.
453 gfx::Point point_in_root = generator->current_location();
454 NSWindow* window = EventGeneratorDelegateMac::GetInstance()->window();
455 NSPoint point_in_window = ConvertRootPointToTarget(window, point_in_root);
456 return [window convertBaseToScreen:point_in_window];
461 @implementation NSApplicationDonor
463 - (NSEvent*)currentEvent {
465 return g_current_event;
467 // Find the original implementation and invoke it.
468 IMP original = EventGeneratorDelegateMac::GetInstance()->CurrentEventMethod();
469 return original(self, _cmd);