Drive: Add BatchableRequest subclass.
[chromium-blink-merge.git] / ui / views / test / event_generator_delegate_mac.mm
blobb36eb53b71ae28c3700a6e2bc6f1e46c58fa6f46
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"
18 namespace {
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;
27 }  // namespace
29 @interface NSEventDonor : NSObject
30 @end
32 @interface NSApplicationDonor : NSObject
33 @end
35 namespace {
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
42   // screen.
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.
58   return modifiers;
61 // Picks the corresponding mouse event type for the buttons set in |flags|.
62 NSEventType PickMouseEventType(int flags,
63                                NSEventType left,
64                                NSEventType right,
65                                NSEventType other) {
66   if (flags & ui::EF_LEFT_MOUSE_BUTTON)
67     return left;
68   if (flags & ui::EF_RIGHT_MOUSE_BUTTON)
69     return right;
70   return other;
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,
76                               int flags,
77                               NSUInteger* modifiers) {
78   if (modifiers)
79     *modifiers = EventFlagsToModifiers(flags);
80   switch (ui_event_type) {
81     case ui::ET_UNKNOWN:
82       return 0;
83     case ui::ET_KEY_PRESSED:
84       return NSKeyDown;
85     case ui::ET_KEY_RELEASED:
86       return NSKeyUp;
87     case ui::ET_MOUSE_PRESSED:
88       return PickMouseEventType(flags,
89                                 NSLeftMouseDown,
90                                 NSRightMouseDown,
91                                 NSOtherMouseDown);
92     case ui::ET_MOUSE_RELEASED:
93       return PickMouseEventType(flags,
94                                 NSLeftMouseUp,
95                                 NSRightMouseUp,
96                                 NSOtherMouseUp);
97     case ui::ET_MOUSE_DRAGGED:
98       return PickMouseEventType(flags,
99                                 NSLeftMouseDragged,
100                                 NSRightMouseDragged,
101                                 NSOtherMouseDragged);
102     case ui::ET_MOUSE_MOVED:
103       return NSMouseMoved;
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;
112     default:
113       NOTREACHED();
114       return 0;
115   }
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]) {
125     case NSKeyDown:
126       [responder keyDown:event];
127       return;
128     case NSKeyUp:
129       [responder keyUp:event];
130       return;
131   }
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];
141       break;
142     case NSRightMouseDown:
143       [responder rightMouseDown:event];
144       break;
145     case NSOtherMouseDown:
146       [responder otherMouseDown:event];
147       break;
148     case NSLeftMouseUp:
149       [responder mouseUp:event];
150       break;
151     case NSRightMouseUp:
152       [responder rightMouseUp:event];
153       break;
154     case NSOtherMouseUp:
155       [responder otherMouseUp:event];
156       break;
157     case NSLeftMouseDragged:
158       [responder mouseDragged:event];
159       break;
160     case NSRightMouseDragged:
161       [responder rightMouseDragged:event];
162       break;
163     case NSOtherMouseDragged:
164       [responder otherMouseDragged:event];
165       break;
166     case NSMouseMoved:
167       // Assumes [NSWindow acceptsMouseMovedEvents] would return YES, and that
168       // NSTrackingAreas have been appropriately installed on |responder|.
169       [responder mouseMoved:event];
170       break;
171     case NSScrollWheel:
172       [responder scrollWheel:event];
173       break;
174     case NSMouseEntered:
175     case NSMouseExited:
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.
180       NOTREACHED();
181       break;
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.
185       NOTREACHED();
186       break;
187     default:
188       NOTREACHED();
189   }
192 NSEvent* CreateMouseEventInWindow(NSWindow* window,
193                                   ui::EventType event_type,
194                                   const gfx::Point& point_in_root,
195                                   int flags) {
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)
200       click_count = 3;
201     else if (flags & ui::EF_IS_DOUBLE_CLICK)
202       click_count = 2;
203     else
204       click_count = 1;
205   }
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
210                             location:point
211                        modifierFlags:modifiers
212                            timestamp:0
213                         windowNumber:[window windowNumber]
214                              context:nil
215                          eventNumber:0
216                           clickCount:click_count
217                             pressure:1.0];
220 // Implementation of ui::test::EventGeneratorDelegate for Mac. Everything
221 // defined inline is just a stub. Interesting overrides are defined below the
222 // class.
223 class EventGeneratorDelegateMac : public ui::EventTarget,
224                                   public ui::EventSource,
225                                   public ui::EventProcessor,
226                                   public ui::EventTargeter,
227                                   public ui::test::EventGeneratorDelegate {
228  public:
229   static EventGeneratorDelegateMac* GetInstance() {
230     return Singleton<EventGeneratorDelegateMac>::get();
231   }
233   IMP CurrentEventMethod() {
234     return swizzle_current_event_->GetOriginalImplementation();
235   }
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 {
262     return this;
263   }
264   ui::EventSource* GetEventSource(ui::EventTarget* target) override {
265     return this;
266   }
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 {}
277  private:
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"]);
298   struct {
299     NSString* title;
300     SEL action;
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"},
309   };
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
314                             atIndex:i];
315   }
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().
326   return nullptr;
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_,
333                                                event->type(),
334                                                event->location(),
335                                                event->changed_button_flags());
336   if (owner_->targeting_application())
337     [NSApp sendEvent:ns_event];
338   else
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(),
346       modifiers);
347   if (owner_->targeting_application()) {
348     [NSApp sendEvent:ns_event];
349     return;
350   }
352   if ([fake_menu_ performKeyEquivalent:ns_event])
353     return;
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();
367   owner_ = owner;
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]];
382   if (owner_) {
383     swizzle_pressed_.reset(new base::mac::ScopedObjCClassSwizzler(
384         [NSEvent class],
385         [NSEventDonor class],
386         @selector(pressedMouseButtons)));
387     swizzle_current_event_.reset(new base::mac::ScopedObjCClassSwizzler(
388         [NSApplication class],
389         [NSApplicationDonor class],
390         @selector(currentEvent)));
391   }
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();
406 }  // namespace
408 namespace views {
409 namespace test {
411 void InitializeMacEventGeneratorDelegate() {
412   EventGeneratorDelegateMac::GetInstance();
415 }  // namespace test
416 }  // namespace views
418 @implementation NSEventDonor
420 // Donate +[NSEvent pressedMouseButtons] by retrieving the flags from the
421 // active generator.
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)
429     bitmask |= 1;
430   if (flags & ui::EF_RIGHT_MOUSE_BUTTON)
431     bitmask |= 1 << 1;
432   if (flags & ui::EF_MIDDLE_MOUSE_BUTTON)
433     bitmask |= 1 << 2;
434   return bitmask;
437 @end
439 @implementation NSApplicationDonor
441 - (NSEvent*)currentEvent {
442   if (g_current_event)
443     return g_current_event;
445   // Find the original implementation and invoke it.
446   IMP original = EventGeneratorDelegateMac::GetInstance()->CurrentEventMethod();
447   return original(self, _cmd);
450 @end