| // Copyright 2014 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import <Cocoa/Cocoa.h> |
| |
| #import "base/mac/scoped_objc_class_swizzler.h" |
| #include "base/memory/singleton.h" |
| #include "ui/events/event_processor.h" |
| #include "ui/events/event_target.h" |
| #include "ui/events/event_target_iterator.h" |
| #include "ui/events/event_targeter.h" |
| #include "ui/events/test/event_generator.h" |
| #include "ui/gfx/mac/coordinate_conversion.h" |
| |
| namespace { |
| |
| // Singleton to provide state for swizzled Objective C methods. |
| ui::test::EventGenerator* g_active_generator = NULL; |
| |
| } // namespace |
| |
| @interface NSEventDonor : NSObject |
| @end |
| |
| @implementation NSEventDonor |
| |
| // Donate +[NSEvent pressedMouseButtons] by retrieving the flags from the |
| // active generator. |
| + (NSUInteger)pressedMouseButtons { |
| int flags = g_active_generator->flags(); |
| NSUInteger bitmask = 0; |
| if (flags & ui::EF_LEFT_MOUSE_BUTTON) |
| bitmask |= 1; |
| if (flags & ui::EF_RIGHT_MOUSE_BUTTON) |
| bitmask |= 1 << 1; |
| if (flags & ui::EF_MIDDLE_MOUSE_BUTTON) |
| bitmask |= 1 << 2; |
| return bitmask; |
| } |
| |
| @end |
| |
| namespace { |
| |
| NSPoint ConvertRootPointToTarget(NSWindow* target, |
| const gfx::Point& point_in_root) { |
| // Normally this would do [NSWindow convertScreenToBase:]. However, Cocoa can |
| // reposition the window on screen and make things flaky. Initially, just |
| // assume that the contentRect of |target| is at the top-left corner of the |
| // screen. |
| NSRect content_rect = [target contentRectForFrameRect:[target frame]]; |
| return NSMakePoint(point_in_root.x(), |
| NSHeight(content_rect) - point_in_root.y()); |
| } |
| |
| // Inverse of ui::EventFlagsFromModifiers(). |
| NSUInteger EventFlagsToModifiers(int flags) { |
| NSUInteger modifiers = 0; |
| modifiers |= (flags & ui::EF_CAPS_LOCK_DOWN) ? NSAlphaShiftKeyMask : 0; |
| modifiers |= (flags & ui::EF_SHIFT_DOWN) ? NSShiftKeyMask : 0; |
| modifiers |= (flags & ui::EF_CONTROL_DOWN) ? NSControlKeyMask : 0; |
| modifiers |= (flags & ui::EF_ALT_DOWN) ? NSAlternateKeyMask : 0; |
| modifiers |= (flags & ui::EF_COMMAND_DOWN) ? NSCommandKeyMask : 0; |
| // ui::EF_*_MOUSE_BUTTON not handled here. |
| // NSFunctionKeyMask, NSNumericPadKeyMask and NSHelpKeyMask not mapped. |
| return modifiers; |
| } |
| |
| // Picks the corresponding mouse event type for the buttons set in |flags|. |
| NSEventType PickMouseEventType(int flags, |
| NSEventType left, |
| NSEventType right, |
| NSEventType other) { |
| if (flags & ui::EF_LEFT_MOUSE_BUTTON) |
| return left; |
| if (flags & ui::EF_RIGHT_MOUSE_BUTTON) |
| return right; |
| return other; |
| } |
| |
| // Inverse of ui::EventTypeFromNative(). If non-null |modifiers| will be set |
| // using the inverse of ui::EventFlagsFromNSEventWithModifiers(). |
| NSEventType EventTypeToNative(ui::EventType ui_event_type, |
| int flags, |
| NSUInteger* modifiers) { |
| if (modifiers) |
| *modifiers = EventFlagsToModifiers(flags); |
| switch (ui_event_type) { |
| case ui::ET_UNKNOWN: |
| return 0; |
| case ui::ET_KEY_PRESSED: |
| return NSKeyDown; |
| case ui::ET_KEY_RELEASED: |
| return NSKeyUp; |
| case ui::ET_MOUSE_PRESSED: |
| return PickMouseEventType(flags, |
| NSLeftMouseDown, |
| NSRightMouseDown, |
| NSOtherMouseDown); |
| case ui::ET_MOUSE_RELEASED: |
| return PickMouseEventType(flags, |
| NSLeftMouseUp, |
| NSRightMouseUp, |
| NSOtherMouseUp); |
| case ui::ET_MOUSE_DRAGGED: |
| return PickMouseEventType(flags, |
| NSLeftMouseDragged, |
| NSRightMouseDragged, |
| NSOtherMouseDragged); |
| case ui::ET_MOUSE_MOVED: |
| return NSMouseMoved; |
| case ui::ET_MOUSEWHEEL: |
| return NSScrollWheel; |
| case ui::ET_MOUSE_ENTERED: |
| return NSMouseEntered; |
| case ui::ET_MOUSE_EXITED: |
| return NSMouseExited; |
| case ui::ET_SCROLL_FLING_START: |
| return NSEventTypeSwipe; |
| default: |
| NOTREACHED(); |
| return 0; |
| } |
| } |
| |
| // Emulate the dispatching that would be performed by -[NSWindow sendEvent:]. |
| // sendEvent is a black box which (among other things) will try to peek at the |
| // event queue and can block indefinitely. |
| void EmulateSendEvent(NSWindow* window, NSEvent* event) { |
| NSResponder* responder = [window firstResponder]; |
| switch ([event type]) { |
| case NSKeyDown: |
| [responder keyDown:event]; |
| return; |
| case NSKeyUp: |
| [responder keyUp:event]; |
| return; |
| } |
| |
| // For mouse events, NSWindow will use -[NSView hitTest:] for the initial |
| // mouseDown, and then keep track of the NSView returned. The toolkit-views |
| // RootView does this too. So, for tests, assume tracking will be done there, |
| // and the NSWindow's contentView is wrapping a views::internal::RootView. |
| responder = [window contentView]; |
| switch ([event type]) { |
| case NSLeftMouseDown: |
| [responder mouseDown:event]; |
| break; |
| case NSRightMouseDown: |
| [responder rightMouseDown:event]; |
| break; |
| case NSOtherMouseDown: |
| [responder otherMouseDown:event]; |
| break; |
| case NSLeftMouseUp: |
| [responder mouseUp:event]; |
| break; |
| case NSRightMouseUp: |
| [responder rightMouseUp:event]; |
| break; |
| case NSOtherMouseUp: |
| [responder otherMouseUp:event]; |
| break; |
| case NSLeftMouseDragged: |
| [responder mouseDragged:event]; |
| break; |
| case NSRightMouseDragged: |
| [responder rightMouseDragged:event]; |
| break; |
| case NSOtherMouseDragged: |
| [responder otherMouseDragged:event]; |
| break; |
| case NSMouseMoved: |
| // Assumes [NSWindow acceptsMouseMovedEvents] would return YES, and that |
| // NSTrackingAreas have been appropriately installed on |responder|. |
| [responder mouseMoved:event]; |
| break; |
| case NSScrollWheel: |
| [responder scrollWheel:event]; |
| break; |
| case NSMouseEntered: |
| case NSMouseExited: |
| // With the assumptions in NSMouseMoved, it doesn't make sense for the |
| // generator to handle entered/exited separately. It's the responsibility |
| // of views::internal::RootView to convert the moved events into entered |
| // and exited events for the individual views. |
| NOTREACHED(); |
| break; |
| case NSEventTypeSwipe: |
| // NSEventTypeSwipe events can't be generated using public interfaces on |
| // NSEvent, so this will need to be handled at a higher level. |
| NOTREACHED(); |
| break; |
| default: |
| NOTREACHED(); |
| } |
| } |
| |
| void DispatchMouseEventInWindow(NSWindow* window, |
| ui::EventType event_type, |
| const gfx::Point& point_in_root, |
| int flags) { |
| NSUInteger click_count = 0; |
| if (event_type == ui::ET_MOUSE_PRESSED || |
| event_type == ui::ET_MOUSE_RELEASED) { |
| if (flags & ui::EF_IS_TRIPLE_CLICK) |
| click_count = 3; |
| else if (flags & ui::EF_IS_DOUBLE_CLICK) |
| click_count = 2; |
| else |
| click_count = 1; |
| } |
| NSPoint point = ConvertRootPointToTarget(window, point_in_root); |
| NSUInteger modifiers = 0; |
| NSEventType type = EventTypeToNative(event_type, flags, &modifiers); |
| NSEvent* event = [NSEvent mouseEventWithType:type |
| location:point |
| modifierFlags:modifiers |
| timestamp:0 |
| windowNumber:[window windowNumber] |
| context:nil |
| eventNumber:0 |
| clickCount:click_count |
| pressure:1.0]; |
| |
| // Typically events go through NSApplication. For tests, dispatch the event |
| // directly to make things more predicatble. |
| EmulateSendEvent(window, event); |
| } |
| |
| // Implementation of ui::test::EventGeneratorDelegate for Mac. Everything |
| // defined inline is just a stub. Interesting overrides are defined below the |
| // class. |
| class EventGeneratorDelegateMac : public ui::EventTarget, |
| public ui::EventSource, |
| public ui::EventProcessor, |
| public ui::EventTargeter, |
| public ui::test::EventGeneratorDelegate { |
| public: |
| static EventGeneratorDelegateMac* GetInstance() { |
| return Singleton<EventGeneratorDelegateMac>::get(); |
| } |
| |
| // Overridden from ui::EventTarget: |
| virtual bool CanAcceptEvent(const ui::Event& event) override { return true; } |
| virtual ui::EventTarget* GetParentTarget() override { return NULL; } |
| virtual scoped_ptr<ui::EventTargetIterator> GetChildIterator() const override; |
| virtual ui::EventTargeter* GetEventTargeter() override { |
| return this; |
| } |
| |
| // Overridden from ui::EventHandler (via ui::EventTarget): |
| virtual void OnMouseEvent(ui::MouseEvent* event) override; |
| |
| // Overridden from ui::EventSource: |
| virtual ui::EventProcessor* GetEventProcessor() override { return this; } |
| |
| // Overridden from ui::EventProcessor: |
| virtual ui::EventTarget* GetRootTarget() override { return this; } |
| |
| // Overridden from ui::EventDispatcherDelegate (via ui::EventProcessor): |
| virtual bool CanDispatchToTarget(EventTarget* target) override { |
| return true; |
| } |
| |
| // Overridden from ui::test::EventGeneratorDelegate: |
| virtual void SetContext(ui::test::EventGenerator* owner, |
| gfx::NativeWindow root_window, |
| gfx::NativeWindow window) override; |
| virtual ui::EventTarget* GetTargetAt(const gfx::Point& location) override { |
| return this; |
| } |
| virtual ui::EventSource* GetEventSource(ui::EventTarget* target) override { |
| return this; |
| } |
| virtual gfx::Point CenterOfTarget( |
| const ui::EventTarget* target) const override; |
| virtual gfx::Point CenterOfWindow(gfx::NativeWindow window) const override; |
| |
| virtual void ConvertPointFromTarget(const ui::EventTarget* target, |
| gfx::Point* point) const override {} |
| virtual void ConvertPointToTarget(const ui::EventTarget* target, |
| gfx::Point* point) const override {} |
| virtual void ConvertPointFromHost(const ui::EventTarget* hosted_target, |
| gfx::Point* point) const override {} |
| |
| private: |
| friend struct DefaultSingletonTraits<EventGeneratorDelegateMac>; |
| |
| EventGeneratorDelegateMac(); |
| virtual ~EventGeneratorDelegateMac(); |
| |
| ui::test::EventGenerator* owner_; |
| NSWindow* window_; |
| scoped_ptr<base::mac::ScopedObjCClassSwizzler> swizzle_pressed_; |
| |
| DISALLOW_COPY_AND_ASSIGN(EventGeneratorDelegateMac); |
| }; |
| |
| EventGeneratorDelegateMac::EventGeneratorDelegateMac() |
| : owner_(NULL), |
| window_(NULL) { |
| DCHECK(!ui::test::EventGenerator::default_delegate); |
| ui::test::EventGenerator::default_delegate = this; |
| } |
| |
| EventGeneratorDelegateMac::~EventGeneratorDelegateMac() { |
| DCHECK_EQ(this, ui::test::EventGenerator::default_delegate); |
| ui::test::EventGenerator::default_delegate = NULL; |
| } |
| |
| scoped_ptr<ui::EventTargetIterator> |
| EventGeneratorDelegateMac::GetChildIterator() const { |
| // Return NULL to dispatch all events to the result of GetRootTarget(). |
| return scoped_ptr<ui::EventTargetIterator>(); |
| } |
| |
| void EventGeneratorDelegateMac::OnMouseEvent(ui::MouseEvent* event) { |
| // For mouse drag events, ensure the swizzled methods return the right flags. |
| base::AutoReset<ui::test::EventGenerator*> reset(&g_active_generator, owner_); |
| DispatchMouseEventInWindow(window_, |
| event->type(), |
| event->location(), |
| event->changed_button_flags()); |
| } |
| |
| void EventGeneratorDelegateMac::SetContext(ui::test::EventGenerator* owner, |
| gfx::NativeWindow root_window, |
| gfx::NativeWindow window) { |
| swizzle_pressed_.reset(); |
| owner_ = owner; |
| window_ = window; |
| if (owner_) { |
| swizzle_pressed_.reset(new base::mac::ScopedObjCClassSwizzler( |
| [NSEvent class], |
| [NSEventDonor class], |
| @selector(pressedMouseButtons))); |
| } |
| } |
| |
| gfx::Point EventGeneratorDelegateMac::CenterOfTarget( |
| const ui::EventTarget* target) const { |
| DCHECK_EQ(target, this); |
| return CenterOfWindow(window_); |
| } |
| |
| gfx::Point EventGeneratorDelegateMac::CenterOfWindow( |
| gfx::NativeWindow window) const { |
| DCHECK_EQ(window, window_); |
| return gfx::ScreenRectFromNSRect([window frame]).CenterPoint(); |
| } |
| |
| } // namespace |
| |
| namespace views { |
| namespace test { |
| |
| void InitializeMacEventGeneratorDelegate() { |
| EventGeneratorDelegateMac::GetInstance(); |
| } |
| |
| } // namespace test |
| } // namespace views |