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 nullptr; }
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::EventTargeter:
258 ui::EventTarget* FindTargetForEvent(ui::EventTarget* root,
259 ui::Event* event) override {
262 ui::EventTarget* FindNextBestTarget(ui::EventTarget* previous_target,
263 ui::Event* event) override {
267 // Overridden from ui::test::EventGeneratorDelegate:
268 void SetContext(ui::test::EventGenerator* owner,
269 gfx::NativeWindow root_window,
270 gfx::NativeWindow window) override;
271 ui::EventTarget* GetTargetAt(const gfx::Point& location) override {
274 ui::EventSource* GetEventSource(ui::EventTarget* target) override {
277 gfx::Point CenterOfTarget(const ui::EventTarget* target) const override;
278 gfx::Point CenterOfWindow(gfx::NativeWindow window) const override;
280 void ConvertPointFromTarget(const ui::EventTarget* target,
281 gfx::Point* point) const override {}
282 void ConvertPointToTarget(const ui::EventTarget* target,
283 gfx::Point* point) const override {}
284 void ConvertPointFromHost(const ui::EventTarget* hosted_target,
285 gfx::Point* point) const override {}
288 friend struct DefaultSingletonTraits<EventGeneratorDelegateMac>;
290 EventGeneratorDelegateMac();
291 ~EventGeneratorDelegateMac() override;
293 ui::test::EventGenerator* owner_;
294 base::scoped_nsobject<NSWindow> window_;
295 scoped_ptr<base::mac::ScopedObjCClassSwizzler> swizzle_pressed_;
296 scoped_ptr<base::mac::ScopedObjCClassSwizzler> swizzle_location_;
297 scoped_ptr<base::mac::ScopedObjCClassSwizzler> swizzle_current_event_;
298 base::scoped_nsobject<NSMenu> fake_menu_;
300 DISALLOW_COPY_AND_ASSIGN(EventGeneratorDelegateMac);
303 EventGeneratorDelegateMac::EventGeneratorDelegateMac() : owner_(nullptr) {
304 DCHECK(!ui::test::EventGenerator::default_delegate);
305 ui::test::EventGenerator::default_delegate = this;
306 // Install a fake "edit" menu. This is normally provided by Chrome's
307 // MainMenu.xib, but src/ui shouldn't depend on that.
308 fake_menu_.reset([[NSMenu alloc] initWithTitle:@"Edit"]);
312 NSString* key_equivalent;
313 } fake_menu_item[] = {
314 {@"Undo", @selector(undo:), @"z"},
315 {@"Redo", @selector(redo:), @"Z"},
316 {@"Copy", @selector(copy:), @"c"},
317 {@"Cut", @selector(cut:), @"x"},
318 {@"Paste", @selector(paste:), @"v"},
319 {@"Select All", @selector(selectAll:), @"a"},
321 for (size_t i = 0; i < arraysize(fake_menu_item); ++i) {
322 [fake_menu_ insertItemWithTitle:fake_menu_item[i].title
323 action:fake_menu_item[i].action
324 keyEquivalent:fake_menu_item[i].key_equivalent
329 EventGeneratorDelegateMac::~EventGeneratorDelegateMac() {
330 DCHECK_EQ(this, ui::test::EventGenerator::default_delegate);
331 ui::test::EventGenerator::default_delegate = nullptr;
334 scoped_ptr<ui::EventTargetIterator>
335 EventGeneratorDelegateMac::GetChildIterator() const {
336 // Return nullptr to dispatch all events to the result of GetRootTarget().
340 void EventGeneratorDelegateMac::OnMouseEvent(ui::MouseEvent* event) {
341 NSEvent* ns_event = CreateMouseEventInWindow(window_,
344 event->changed_button_flags());
345 if (owner_->targeting_application())
346 [NSApp sendEvent:ns_event];
348 EmulateSendEvent(window_, ns_event);
351 void EventGeneratorDelegateMac::OnKeyEvent(ui::KeyEvent* event) {
352 NSUInteger modifiers = EventFlagsToModifiers(event->flags());
353 NSEvent* ns_event = cocoa_test_event_utils::SynthesizeKeyEvent(
354 window_, event->type() == ui::ET_KEY_PRESSED, event->key_code(),
356 if (owner_->targeting_application()) {
357 [NSApp sendEvent:ns_event];
361 if ([fake_menu_ performKeyEquivalent:ns_event])
364 EmulateSendEvent(window_, ns_event);
367 void EventGeneratorDelegateMac::OnTouchEvent(ui::TouchEvent* event) {
368 NOTREACHED() << "Touchscreen events not supported on Chrome Mac.";
371 void EventGeneratorDelegateMac::SetContext(ui::test::EventGenerator* owner,
372 gfx::NativeWindow root_window,
373 gfx::NativeWindow window) {
374 swizzle_pressed_.reset();
375 swizzle_location_.reset();
376 swizzle_current_event_.reset();
379 // Retain the NSWindow (note it can be nil). This matches Cocoa's tendency to
380 // have autoreleased objects, or objects still in the event queue, that
381 // reference the NSWindow.
382 window_.reset([window retain]);
384 // Normally, edit menu items have a `nil` target. This results in -[NSMenu
385 // performKeyEquivalent:] relying on -[NSApplication targetForAction:to:from:]
386 // to find a target starting at the first responder of the key window. Since
387 // non-interactive tests have no key window, that won't work. So set (or
388 // clear) the target explicitly on all menu items.
389 [[fake_menu_ itemArray] makeObjectsPerformSelector:@selector(setTarget:)
390 withObject:[window firstResponder]];
393 swizzle_pressed_.reset(new base::mac::ScopedObjCClassSwizzler(
395 [NSEventDonor class],
396 @selector(pressedMouseButtons)));
397 swizzle_location_.reset(new base::mac::ScopedObjCClassSwizzler(
398 [NSEvent class], [NSEventDonor class], @selector(mouseLocation)));
399 swizzle_current_event_.reset(new base::mac::ScopedObjCClassSwizzler(
400 [NSApplication class],
401 [NSApplicationDonor class],
402 @selector(currentEvent)));
406 gfx::Point EventGeneratorDelegateMac::CenterOfTarget(
407 const ui::EventTarget* target) const {
408 DCHECK_EQ(target, this);
409 return CenterOfWindow(window_);
412 gfx::Point EventGeneratorDelegateMac::CenterOfWindow(
413 gfx::NativeWindow window) const {
414 DCHECK_EQ(window, window_);
415 return gfx::ScreenRectFromNSRect([window frame]).CenterPoint();
418 // Return the current owner of the EventGeneratorDelegate. May be null.
419 ui::test::EventGenerator* GetActiveGenerator() {
420 return EventGeneratorDelegateMac::GetInstance()->owner();
428 void InitializeMacEventGeneratorDelegate() {
429 EventGeneratorDelegateMac::GetInstance();
435 @implementation NSEventDonor
437 // Donate +[NSEvent pressedMouseButtons] by retrieving the flags from the
439 + (NSUInteger)pressedMouseButtons {
440 ui::test::EventGenerator* generator = GetActiveGenerator();
442 return [NSEventDonor pressedMouseButtons]; // Call original implementation.
444 int flags = generator->flags();
445 NSUInteger bitmask = 0;
446 if (flags & ui::EF_LEFT_MOUSE_BUTTON)
448 if (flags & ui::EF_RIGHT_MOUSE_BUTTON)
450 if (flags & ui::EF_MIDDLE_MOUSE_BUTTON)
455 // Donate +[NSEvent mouseLocation] by retrieving the current position on screen.
456 + (NSPoint)mouseLocation {
457 ui::test::EventGenerator* generator = GetActiveGenerator();
459 return [NSEventDonor mouseLocation]; // Call original implementation.
461 // The location is the point in the root window which, for desktop widgets, is
462 // the widget itself.
463 gfx::Point point_in_root = generator->current_location();
464 NSWindow* window = EventGeneratorDelegateMac::GetInstance()->window();
465 NSPoint point_in_window = ConvertRootPointToTarget(window, point_in_root);
466 return [window convertBaseToScreen:point_in_window];
471 @implementation NSApplicationDonor
473 - (NSEvent*)currentEvent {
475 return g_current_event;
477 // Find the original implementation and invoke it.
478 IMP original = EventGeneratorDelegateMac::GetInstance()->CurrentEventMethod();
479 return original(self, _cmd);