| // 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. |
| |
| #include "ui/events/event_utils.h" |
| |
| #import <Cocoa/Cocoa.h> |
| |
| #include "base/mac/scoped_cftyperef.h" |
| #import "base/mac/scoped_objc_class_swizzler.h" |
| #include "base/memory/scoped_ptr.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "ui/events/event_constants.h" |
| #import "ui/events/test/cocoa_test_event_utils.h" |
| #include "ui/gfx/point.h" |
| #import "ui/gfx/test/ui_cocoa_test_helper.h" |
| |
| namespace { |
| |
| NSWindow* g_test_window = nil; |
| |
| } // namespace |
| |
| // Mac APIs for creating test events are frustrating. Quartz APIs have, e.g., |
| // CGEventCreateMouseEvent() which can't set a window or modifier flags. |
| // Cocoa APIs have +[NSEvent mouseEventWithType:..] which can't set |
| // buttonNumber or scroll deltas. To work around this, these tests use some |
| // Objective C magic to donate member functions to NSEvent temporarily. |
| @interface MiddleMouseButtonNumberDonor : NSObject |
| @end |
| |
| @interface TestWindowDonor : NSObject |
| @end |
| |
| @implementation MiddleMouseButtonNumberDonor |
| - (NSInteger)buttonNumber { return 2; } |
| @end |
| |
| @implementation TestWindowDonor |
| - (NSWindow*)window { return g_test_window; } |
| @end |
| |
| namespace ui { |
| |
| namespace { |
| |
| class EventsMacTest : public CocoaTest { |
| public: |
| EventsMacTest() {} |
| |
| gfx::Point Flip(gfx::Point window_location) { |
| NSRect window_frame = [test_window() frame]; |
| CGFloat content_height = |
| NSHeight([test_window() contentRectForFrameRect:window_frame]); |
| window_location.set_y(content_height - window_location.y()); |
| return window_location; |
| } |
| |
| void SwizzleMiddleMouseButton() { |
| DCHECK(!swizzler_); |
| swizzler_.reset(new base::mac::ScopedObjCClassSwizzler( |
| [NSEvent class], |
| [MiddleMouseButtonNumberDonor class], |
| @selector(buttonNumber))); |
| } |
| |
| void SwizzleTestWindow() { |
| DCHECK(!g_test_window); |
| DCHECK(!swizzler_); |
| g_test_window = test_window(); |
| swizzler_.reset(new base::mac::ScopedObjCClassSwizzler( |
| [NSEvent class], [TestWindowDonor class], @selector(window))); |
| } |
| |
| void ClearSwizzle() { |
| swizzler_.reset(); |
| g_test_window = nil; |
| } |
| |
| NSEvent* TestMouseEvent(NSEventType type, |
| const gfx::Point &window_location, |
| NSInteger modifier_flags) { |
| NSPoint point = NSPointFromCGPoint(Flip(window_location).ToCGPoint()); |
| return [NSEvent mouseEventWithType:type |
| location:point |
| modifierFlags:modifier_flags |
| timestamp:0 |
| windowNumber:[test_window() windowNumber] |
| context:nil |
| eventNumber:0 |
| clickCount:0 |
| pressure:1.0]; |
| } |
| |
| NSEvent* TestScrollEvent(const gfx::Point& window_location, |
| int32_t delta_x, |
| int32_t delta_y) { |
| SwizzleTestWindow(); |
| base::ScopedCFTypeRef<CGEventRef> scroll( |
| CGEventCreateScrollWheelEvent(NULL, |
| kCGScrollEventUnitLine, |
| 2, |
| delta_y, |
| delta_x)); |
| // CGEvents are always in global display coordinates. These are like screen |
| // coordinates, but flipped. But first the point needs to be converted out |
| // of window coordinates (which also requires flipping). |
| NSPoint window_point = |
| NSPointFromCGPoint(Flip(window_location).ToCGPoint()); |
| NSPoint screen_point = [test_window() convertBaseToScreen:window_point]; |
| CGFloat primary_screen_height = |
| NSHeight([[[NSScreen screens] objectAtIndex:0] frame]); |
| screen_point.y = primary_screen_height - screen_point.y; |
| CGEventSetLocation(scroll, NSPointToCGPoint(screen_point)); |
| return [NSEvent eventWithCGEvent:scroll]; |
| } |
| |
| private: |
| scoped_ptr<base::mac::ScopedObjCClassSwizzler> swizzler_; |
| |
| DISALLOW_COPY_AND_ASSIGN(EventsMacTest); |
| }; |
| |
| } // namespace |
| |
| TEST_F(EventsMacTest, EventFlagsFromNative) { |
| // Left click. |
| NSEvent* left = cocoa_test_event_utils::MouseEventWithType(NSLeftMouseUp, 0); |
| EXPECT_EQ(EF_LEFT_MOUSE_BUTTON, EventFlagsFromNative(left)); |
| |
| // Right click. |
| NSEvent* right = cocoa_test_event_utils::MouseEventWithType(NSRightMouseUp, |
| 0); |
| EXPECT_EQ(EF_RIGHT_MOUSE_BUTTON, EventFlagsFromNative(right)); |
| |
| // Middle click. |
| NSEvent* middle = cocoa_test_event_utils::MouseEventWithType(NSOtherMouseUp, |
| 0); |
| EXPECT_EQ(EF_MIDDLE_MOUSE_BUTTON, EventFlagsFromNative(middle)); |
| |
| // Caps + Left |
| NSEvent* caps = cocoa_test_event_utils::MouseEventWithType( |
| NSLeftMouseUp, NSAlphaShiftKeyMask); |
| EXPECT_EQ(EF_LEFT_MOUSE_BUTTON | EF_CAPS_LOCK_DOWN, |
| EventFlagsFromNative(caps)); |
| |
| // Shift + Left |
| NSEvent* shift = cocoa_test_event_utils::MouseEventWithType(NSLeftMouseUp, |
| NSShiftKeyMask); |
| EXPECT_EQ(EF_LEFT_MOUSE_BUTTON | EF_SHIFT_DOWN, EventFlagsFromNative(shift)); |
| |
| // Ctrl + Left |
| NSEvent* ctrl = cocoa_test_event_utils::MouseEventWithType(NSLeftMouseUp, |
| NSControlKeyMask); |
| EXPECT_EQ(EF_LEFT_MOUSE_BUTTON | EF_CONTROL_DOWN, EventFlagsFromNative(ctrl)); |
| |
| // Alt + Left |
| NSEvent* alt = cocoa_test_event_utils::MouseEventWithType(NSLeftMouseUp, |
| NSAlternateKeyMask); |
| EXPECT_EQ(EF_LEFT_MOUSE_BUTTON | EF_ALT_DOWN, EventFlagsFromNative(alt)); |
| |
| // Cmd + Left |
| NSEvent* cmd = cocoa_test_event_utils::MouseEventWithType(NSLeftMouseUp, |
| NSCommandKeyMask); |
| EXPECT_EQ(EF_LEFT_MOUSE_BUTTON | EF_COMMAND_DOWN, EventFlagsFromNative(cmd)); |
| |
| // Shift + Ctrl + Left |
| NSEvent* shiftctrl = cocoa_test_event_utils::MouseEventWithType( |
| NSLeftMouseUp, NSShiftKeyMask | NSControlKeyMask); |
| EXPECT_EQ(EF_LEFT_MOUSE_BUTTON | EF_SHIFT_DOWN | EF_CONTROL_DOWN, |
| EventFlagsFromNative(shiftctrl)); |
| |
| // Cmd + Alt + Right |
| NSEvent* cmdalt = cocoa_test_event_utils::MouseEventWithType( |
| NSLeftMouseUp, NSCommandKeyMask | NSAlternateKeyMask); |
| EXPECT_EQ(EF_LEFT_MOUSE_BUTTON | EF_COMMAND_DOWN | EF_ALT_DOWN, |
| EventFlagsFromNative(cmdalt)); |
| } |
| |
| // Tests mouse button presses and mouse wheel events. |
| TEST_F(EventsMacTest, ButtonEvents) { |
| gfx::Point location(5, 10); |
| gfx::Vector2d offset; |
| |
| NSEvent* event = TestMouseEvent(NSLeftMouseDown, location, 0); |
| EXPECT_EQ(ui::ET_MOUSE_PRESSED, ui::EventTypeFromNative(event)); |
| EXPECT_EQ(ui::EF_LEFT_MOUSE_BUTTON, ui::EventFlagsFromNative(event)); |
| EXPECT_EQ(location, ui::EventLocationFromNative(event)); |
| |
| SwizzleMiddleMouseButton(); |
| event = TestMouseEvent(NSOtherMouseDown, location, NSShiftKeyMask); |
| EXPECT_EQ(ui::ET_MOUSE_PRESSED, ui::EventTypeFromNative(event)); |
| EXPECT_EQ(ui::EF_MIDDLE_MOUSE_BUTTON | ui::EF_SHIFT_DOWN, |
| ui::EventFlagsFromNative(event)); |
| EXPECT_EQ(location, ui::EventLocationFromNative(event)); |
| ClearSwizzle(); |
| |
| event = TestMouseEvent(NSRightMouseUp, location, 0); |
| EXPECT_EQ(ui::ET_MOUSE_RELEASED, ui::EventTypeFromNative(event)); |
| EXPECT_EQ(ui::EF_RIGHT_MOUSE_BUTTON, ui::EventFlagsFromNative(event)); |
| EXPECT_EQ(location, ui::EventLocationFromNative(event)); |
| |
| // Scroll up. |
| event = TestScrollEvent(location, 0, 1); |
| EXPECT_EQ(ui::ET_MOUSEWHEEL, ui::EventTypeFromNative(event)); |
| EXPECT_EQ(0, ui::EventFlagsFromNative(event)); |
| EXPECT_EQ(location.ToString(), ui::EventLocationFromNative(event).ToString()); |
| offset = ui::GetMouseWheelOffset(event); |
| EXPECT_GT(offset.y(), 0); |
| EXPECT_EQ(0, offset.x()); |
| ClearSwizzle(); |
| |
| // Scroll down. |
| event = TestScrollEvent(location, 0, -1); |
| EXPECT_EQ(ui::ET_MOUSEWHEEL, ui::EventTypeFromNative(event)); |
| EXPECT_EQ(0, ui::EventFlagsFromNative(event)); |
| EXPECT_EQ(location, ui::EventLocationFromNative(event)); |
| offset = ui::GetMouseWheelOffset(event); |
| EXPECT_LT(offset.y(), 0); |
| EXPECT_EQ(0, offset.x()); |
| ClearSwizzle(); |
| |
| // Scroll left. |
| event = TestScrollEvent(location, 1, 0); |
| EXPECT_EQ(ui::ET_MOUSEWHEEL, ui::EventTypeFromNative(event)); |
| EXPECT_EQ(0, ui::EventFlagsFromNative(event)); |
| EXPECT_EQ(location, ui::EventLocationFromNative(event)); |
| offset = ui::GetMouseWheelOffset(event); |
| EXPECT_EQ(0, offset.y()); |
| EXPECT_GT(offset.x(), 0); |
| ClearSwizzle(); |
| |
| // Scroll right. |
| event = TestScrollEvent(location, -1, 0); |
| EXPECT_EQ(ui::ET_MOUSEWHEEL, ui::EventTypeFromNative(event)); |
| EXPECT_EQ(0, ui::EventFlagsFromNative(event)); |
| EXPECT_EQ(location, ui::EventLocationFromNative(event)); |
| offset = ui::GetMouseWheelOffset(event); |
| EXPECT_EQ(0, offset.y()); |
| EXPECT_LT(offset.x(), 0); |
| ClearSwizzle(); |
| } |
| |
| // Test correct location when the window has a native titlebar. |
| TEST_F(EventsMacTest, NativeTitlebarEventLocation) { |
| gfx::Point location(5, 10); |
| NSUInteger style_mask = NSTitledWindowMask | NSClosableWindowMask | |
| NSMiniaturizableWindowMask | NSResizableWindowMask; |
| |
| // First check that the window provided by ui::CocoaTest is how we think. |
| DCHECK_EQ(NSBorderlessWindowMask, [test_window() styleMask]); |
| [test_window() setStyleMask:style_mask]; |
| DCHECK_EQ(style_mask, [test_window() styleMask]); |
| |
| // EventLocationFromNative should behave the same as the ButtonEvents test. |
| NSEvent* event = TestMouseEvent(NSLeftMouseDown, location, 0); |
| EXPECT_EQ(ui::ET_MOUSE_PRESSED, ui::EventTypeFromNative(event)); |
| EXPECT_EQ(ui::EF_LEFT_MOUSE_BUTTON, ui::EventFlagsFromNative(event)); |
| EXPECT_EQ(location, ui::EventLocationFromNative(event)); |
| |
| // And be explicit, to ensure the test doesn't depend on some property of the |
| // test harness. The change to the frame rect could be OS-specfic, so set it |
| // to a known value. |
| const CGFloat kTestHeight = 400; |
| NSRect content_rect = NSMakeRect(0, 0, 600, kTestHeight); |
| NSRect frame_rect = [test_window() frameRectForContentRect:content_rect]; |
| [test_window() setFrame:frame_rect display:YES]; |
| event = [NSEvent mouseEventWithType:NSLeftMouseDown |
| location:NSMakePoint(0, 0) // Bottom-left corner. |
| modifierFlags:0 |
| timestamp:0 |
| windowNumber:[test_window() windowNumber] |
| context:nil |
| eventNumber:0 |
| clickCount:0 |
| pressure:1.0]; |
| // Bottom-left corner should be flipped. |
| EXPECT_EQ(gfx::Point(0, kTestHeight), ui::EventLocationFromNative(event)); |
| |
| // Removing the border, and sending the same event should move it down in the |
| // toolkit-views coordinate system. |
| int height_change = NSHeight(frame_rect) - kTestHeight; |
| EXPECT_GT(height_change, 0); |
| [test_window() setStyleMask:NSBorderlessWindowMask]; |
| [test_window() setFrame:frame_rect display:YES]; |
| EXPECT_EQ(gfx::Point(0, kTestHeight + height_change), |
| ui::EventLocationFromNative(event)); |
| } |
| |
| // Testing for ui::EventTypeFromNative() not covered by ButtonEvents. |
| TEST_F(EventsMacTest, EventTypeFromNative) { |
| NSEvent* event = cocoa_test_event_utils::KeyEventWithType(NSKeyDown, 0); |
| EXPECT_EQ(ui::ET_KEY_PRESSED, ui::EventTypeFromNative(event)); |
| |
| event = cocoa_test_event_utils::KeyEventWithType(NSKeyUp, 0); |
| EXPECT_EQ(ui::ET_KEY_RELEASED, ui::EventTypeFromNative(event)); |
| |
| event = cocoa_test_event_utils::MouseEventWithType(NSLeftMouseDragged, 0); |
| EXPECT_EQ(ui::ET_MOUSE_DRAGGED, ui::EventTypeFromNative(event)); |
| event = cocoa_test_event_utils::MouseEventWithType(NSRightMouseDragged, 0); |
| EXPECT_EQ(ui::ET_MOUSE_DRAGGED, ui::EventTypeFromNative(event)); |
| event = cocoa_test_event_utils::MouseEventWithType(NSOtherMouseDragged, 0); |
| EXPECT_EQ(ui::ET_MOUSE_DRAGGED, ui::EventTypeFromNative(event)); |
| |
| event = cocoa_test_event_utils::MouseEventWithType(NSMouseMoved, 0); |
| EXPECT_EQ(ui::ET_MOUSE_MOVED, ui::EventTypeFromNative(event)); |
| |
| event = cocoa_test_event_utils::EnterExitEventWithType(NSMouseEntered); |
| EXPECT_EQ(ui::ET_MOUSE_ENTERED, ui::EventTypeFromNative(event)); |
| event = cocoa_test_event_utils::EnterExitEventWithType(NSMouseExited); |
| EXPECT_EQ(ui::ET_MOUSE_EXITED, ui::EventTypeFromNative(event)); |
| } |
| |
| } // namespace ui |