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;
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_current_event_;
287 base::scoped_nsobject<NSMenu> fake_menu_;
289 DISALLOW_COPY_AND_ASSIGN(EventGeneratorDelegateMac);
292 EventGeneratorDelegateMac::EventGeneratorDelegateMac() : owner_(NULL) {
293 DCHECK(!ui::test::EventGenerator::default_delegate);
294 ui::test::EventGenerator::default_delegate = this;
295 // Install a fake "edit" menu. This is normally provided by Chrome's
296 // MainMenu.xib, but src/ui shouldn't depend on that.
297 fake_menu_.reset([[NSMenu alloc] initWithTitle:@"Edit"]);
301 NSString* key_equivalent;
302 } fake_menu_item[] = {
303 {@"Undo", @selector(undo:), @"z"},
304 {@"Redo", @selector(redo:), @"Z"},
305 {@"Copy", @selector(copy:), @"c"},
306 {@"Cut", @selector(cut:), @"x"},
307 {@"Paste", @selector(paste:), @"v"},
308 {@"Select All", @selector(selectAll:), @"a"},
310 for (size_t i = 0; i < arraysize(fake_menu_item); ++i) {
311 [fake_menu_ insertItemWithTitle:fake_menu_item[i].title
312 action:fake_menu_item[i].action
313 keyEquivalent:fake_menu_item[i].key_equivalent
318 EventGeneratorDelegateMac::~EventGeneratorDelegateMac() {
319 DCHECK_EQ(this, ui::test::EventGenerator::default_delegate);
320 ui::test::EventGenerator::default_delegate = NULL;
323 scoped_ptr<ui::EventTargetIterator>
324 EventGeneratorDelegateMac::GetChildIterator() const {
325 // Return NULL to dispatch all events to the result of GetRootTarget().
329 void EventGeneratorDelegateMac::OnMouseEvent(ui::MouseEvent* event) {
330 // For mouse drag events, ensure the swizzled methods return the right flags.
331 base::AutoReset<ui::test::EventGenerator*> reset(&g_active_generator, owner_);
332 NSEvent* ns_event = CreateMouseEventInWindow(window_,
335 event->changed_button_flags());
336 if (owner_->targeting_application())
337 [NSApp sendEvent:ns_event];
339 EmulateSendEvent(window_, ns_event);
342 void EventGeneratorDelegateMac::OnKeyEvent(ui::KeyEvent* event) {
343 NSUInteger modifiers = EventFlagsToModifiers(event->flags());
344 NSEvent* ns_event = cocoa_test_event_utils::SynthesizeKeyEvent(
345 window_, event->type() == ui::ET_KEY_PRESSED, event->key_code(),
347 if (owner_->targeting_application()) {
348 [NSApp sendEvent:ns_event];
352 if ([fake_menu_ performKeyEquivalent:ns_event])
355 EmulateSendEvent(window_, ns_event);
358 void EventGeneratorDelegateMac::OnTouchEvent(ui::TouchEvent* event) {
359 NOTREACHED() << "Touchscreen events not supported on Chrome Mac.";
362 void EventGeneratorDelegateMac::SetContext(ui::test::EventGenerator* owner,
363 gfx::NativeWindow root_window,
364 gfx::NativeWindow window) {
365 swizzle_pressed_.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_current_event_.reset(new base::mac::ScopedObjCClassSwizzler(
388 [NSApplication class],
389 [NSApplicationDonor class],
390 @selector(currentEvent)));
394 gfx::Point EventGeneratorDelegateMac::CenterOfTarget(
395 const ui::EventTarget* target) const {
396 DCHECK_EQ(target, this);
397 return CenterOfWindow(window_);
400 gfx::Point EventGeneratorDelegateMac::CenterOfWindow(
401 gfx::NativeWindow window) const {
402 DCHECK_EQ(window, window_);
403 return gfx::ScreenRectFromNSRect([window frame]).CenterPoint();
411 void InitializeMacEventGeneratorDelegate() {
412 EventGeneratorDelegateMac::GetInstance();
418 @implementation NSEventDonor
420 // Donate +[NSEvent pressedMouseButtons] by retrieving the flags from the
422 + (NSUInteger)pressedMouseButtons {
423 if (!g_active_generator)
424 return [NSEventDonor pressedMouseButtons]; // Call original implementation.
426 int flags = g_active_generator->flags();
427 NSUInteger bitmask = 0;
428 if (flags & ui::EF_LEFT_MOUSE_BUTTON)
430 if (flags & ui::EF_RIGHT_MOUSE_BUTTON)
432 if (flags & ui::EF_MIDDLE_MOUSE_BUTTON)
439 @implementation NSApplicationDonor
441 - (NSEvent*)currentEvent {
443 return g_current_event;
445 // Find the original implementation and invoke it.
446 IMP original = EventGeneratorDelegateMac::GetInstance()->CurrentEventMethod();
447 return original(self, _cmd);