Add ICU message format support
[chromium-blink-merge.git] / ui / views / test / event_generator_delegate_mac.mm
blob4e4a32b2237f48ba26faa325af59c772801eabf5
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 // Set (and always cleared) in EmulateSendEvent() to provide an answer for
21 // [NSApp currentEvent].
22 NSEvent* g_current_event = nil;
24 }  // namespace
26 @interface NSEventDonor : NSObject
27 @end
29 @interface NSApplicationDonor : NSObject
30 @end
32 namespace {
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
39   // screen.
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.
55   return modifiers;
58 // Picks the corresponding mouse event type for the buttons set in |flags|.
59 NSEventType PickMouseEventType(int flags,
60                                NSEventType left,
61                                NSEventType right,
62                                NSEventType other) {
63   if (flags & ui::EF_LEFT_MOUSE_BUTTON)
64     return left;
65   if (flags & ui::EF_RIGHT_MOUSE_BUTTON)
66     return right;
67   return other;
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,
73                               int flags,
74                               NSUInteger* modifiers) {
75   if (modifiers)
76     *modifiers = EventFlagsToModifiers(flags);
77   switch (ui_event_type) {
78     case ui::ET_KEY_PRESSED:
79       return NSKeyDown;
80     case ui::ET_KEY_RELEASED:
81       return NSKeyUp;
82     case ui::ET_MOUSE_PRESSED:
83       return PickMouseEventType(flags,
84                                 NSLeftMouseDown,
85                                 NSRightMouseDown,
86                                 NSOtherMouseDown);
87     case ui::ET_MOUSE_RELEASED:
88       return PickMouseEventType(flags,
89                                 NSLeftMouseUp,
90                                 NSRightMouseUp,
91                                 NSOtherMouseUp);
92     case ui::ET_MOUSE_DRAGGED:
93       return PickMouseEventType(flags,
94                                 NSLeftMouseDragged,
95                                 NSRightMouseDragged,
96                                 NSOtherMouseDragged);
97     case ui::ET_MOUSE_MOVED:
98       return NSMouseMoved;
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;
107     default:
108       NOTREACHED();
109       return NSApplicationDefined;
110   }
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]) {
120     case NSKeyDown:
121       [responder keyDown:event];
122       return;
123     case NSKeyUp:
124       [responder keyUp:event];
125       return;
126     default:
127       break;
128   }
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];
138       break;
139     case NSRightMouseDown:
140       [responder rightMouseDown:event];
141       break;
142     case NSOtherMouseDown:
143       [responder otherMouseDown:event];
144       break;
145     case NSLeftMouseUp:
146       [responder mouseUp:event];
147       break;
148     case NSRightMouseUp:
149       [responder rightMouseUp:event];
150       break;
151     case NSOtherMouseUp:
152       [responder otherMouseUp:event];
153       break;
154     case NSLeftMouseDragged:
155       [responder mouseDragged:event];
156       break;
157     case NSRightMouseDragged:
158       [responder rightMouseDragged:event];
159       break;
160     case NSOtherMouseDragged:
161       [responder otherMouseDragged:event];
162       break;
163     case NSMouseMoved:
164       // Assumes [NSWindow acceptsMouseMovedEvents] would return YES, and that
165       // NSTrackingAreas have been appropriately installed on |responder|.
166       [responder mouseMoved:event];
167       break;
168     case NSScrollWheel:
169       [responder scrollWheel:event];
170       break;
171     case NSMouseEntered:
172     case NSMouseExited:
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.
177       NOTREACHED();
178       break;
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.
182       NOTREACHED();
183       break;
184     default:
185       NOTREACHED();
186   }
189 NSEvent* CreateMouseEventInWindow(NSWindow* window,
190                                   ui::EventType event_type,
191                                   const gfx::Point& point_in_root,
192                                   int flags) {
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)
197       click_count = 3;
198     else if (flags & ui::EF_IS_DOUBLE_CLICK)
199       click_count = 2;
200     else
201       click_count = 1;
202   }
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
207                             location:point
208                        modifierFlags:modifiers
209                            timestamp:0
210                         windowNumber:[window windowNumber]
211                              context:nil
212                          eventNumber:0
213                           clickCount:click_count
214                             pressure:1.0];
217 // Implementation of ui::test::EventGeneratorDelegate for Mac. Everything
218 // defined inline is just a stub. Interesting overrides are defined below the
219 // class.
220 class EventGeneratorDelegateMac : public ui::EventTarget,
221                                   public ui::EventSource,
222                                   public ui::EventProcessor,
223                                   public ui::EventTargeter,
224                                   public ui::test::EventGeneratorDelegate {
225  public:
226   static EventGeneratorDelegateMac* GetInstance() {
227     return Singleton<EventGeneratorDelegateMac>::get();
228   }
230   IMP CurrentEventMethod() {
231     return swizzle_current_event_->GetOriginalImplementation();
232   }
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 {
260     return root;
261   }
262   ui::EventTarget* FindNextBestTarget(ui::EventTarget* previous_target,
263                                       ui::Event* event) override {
264     return nullptr;
265   }
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 {
272     return this;
273   }
274   ui::EventSource* GetEventSource(ui::EventTarget* target) override {
275     return this;
276   }
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 {}
287  private:
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"]);
309   struct {
310     NSString* title;
311     SEL action;
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"},
320   };
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
325                             atIndex:i];
326   }
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().
337   return nullptr;
340 void EventGeneratorDelegateMac::OnMouseEvent(ui::MouseEvent* event) {
341   NSEvent* ns_event = CreateMouseEventInWindow(window_,
342                                                event->type(),
343                                                event->location(),
344                                                event->changed_button_flags());
345   if (owner_->targeting_application())
346     [NSApp sendEvent:ns_event];
347   else
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(),
355       modifiers);
356   if (owner_->targeting_application()) {
357     [NSApp sendEvent:ns_event];
358     return;
359   }
361   if ([fake_menu_ performKeyEquivalent:ns_event])
362     return;
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();
377   owner_ = owner;
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]];
392   if (owner_) {
393     swizzle_pressed_.reset(new base::mac::ScopedObjCClassSwizzler(
394         [NSEvent class],
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)));
403   }
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();
423 }  // namespace
425 namespace views {
426 namespace test {
428 void InitializeMacEventGeneratorDelegate() {
429   EventGeneratorDelegateMac::GetInstance();
432 }  // namespace test
433 }  // namespace views
435 @implementation NSEventDonor
437 // Donate +[NSEvent pressedMouseButtons] by retrieving the flags from the
438 // active generator.
439 + (NSUInteger)pressedMouseButtons {
440   ui::test::EventGenerator* generator = GetActiveGenerator();
441   if (!generator)
442     return [NSEventDonor pressedMouseButtons];  // Call original implementation.
444   int flags = generator->flags();
445   NSUInteger bitmask = 0;
446   if (flags & ui::EF_LEFT_MOUSE_BUTTON)
447     bitmask |= 1;
448   if (flags & ui::EF_RIGHT_MOUSE_BUTTON)
449     bitmask |= 1 << 1;
450   if (flags & ui::EF_MIDDLE_MOUSE_BUTTON)
451     bitmask |= 1 << 2;
452   return bitmask;
455 // Donate +[NSEvent mouseLocation] by retrieving the current position on screen.
456 + (NSPoint)mouseLocation {
457   ui::test::EventGenerator* generator = GetActiveGenerator();
458   if (!generator)
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];
469 @end
471 @implementation NSApplicationDonor
473 - (NSEvent*)currentEvent {
474   if (g_current_event)
475     return g_current_event;
477   // Find the original implementation and invoke it.
478   IMP original = EventGeneratorDelegateMac::GetInstance()->CurrentEventMethod();
479   return original(self, _cmd);
482 @end