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. 4 5 #include "ui/events/event_utils.h" 6 7 #import <Cocoa/Cocoa.h> 8 9 #include "base/mac/scoped_cftyperef.h" 10 #import "base/mac/scoped_objc_class_swizzler.h" 11 #include "base/memory/scoped_ptr.h" 12 #include "testing/gtest/include/gtest/gtest.h" 13 #include "ui/events/event_constants.h" 14 #import "ui/events/test/cocoa_test_event_utils.h" 15 #include "ui/gfx/point.h" 16 #import "ui/gfx/test/ui_cocoa_test_helper.h" 17 18 namespace { 19 20 NSWindow* g_test_window = nil; 21 22 } // namespace 23 24 // Mac APIs for creating test events are frustrating. Quartz APIs have, e.g., 25 // CGEventCreateMouseEvent() which can't set a window or modifier flags. 26 // Cocoa APIs have +[NSEvent mouseEventWithType:..] which can't set 27 // buttonNumber or scroll deltas. To work around this, these tests use some 28 // Objective C magic to donate member functions to NSEvent temporarily. 29 @interface MiddleMouseButtonNumberDonor : NSObject 30 @end 31 32 @interface TestWindowDonor : NSObject 33 @end 34 35 @implementation MiddleMouseButtonNumberDonor 36 - (NSInteger)buttonNumber { return 2; } 37 @end 38 39 @implementation TestWindowDonor 40 - (NSWindow*)window { return g_test_window; } 41 @end 42 43 namespace ui { 44 45 namespace { 46 47 class EventsMacTest : public CocoaTest { 48 public: 49 EventsMacTest() {} 50 51 gfx::Point Flip(gfx::Point window_location) { 52 NSRect window_frame = [test_window() frame]; 53 CGFloat content_height = 54 NSHeight([test_window() contentRectForFrameRect:window_frame]); 55 window_location.set_y(content_height - window_location.y()); 56 return window_location; 57 } 58 59 void SwizzleMiddleMouseButton() { 60 DCHECK(!swizzler_); 61 swizzler_.reset(new base::mac::ScopedObjCClassSwizzler( 62 [NSEvent class], 63 [MiddleMouseButtonNumberDonor class], 64 @selector(buttonNumber))); 65 } 66 67 void SwizzleTestWindow() { 68 DCHECK(!g_test_window); 69 DCHECK(!swizzler_); 70 g_test_window = test_window(); 71 swizzler_.reset(new base::mac::ScopedObjCClassSwizzler( 72 [NSEvent class], [TestWindowDonor class], @selector(window))); 73 } 74 75 void ClearSwizzle() { 76 swizzler_.reset(); 77 g_test_window = nil; 78 } 79 80 NSEvent* TestMouseEvent(NSEventType type, 81 const gfx::Point &window_location, 82 NSInteger modifier_flags) { 83 NSPoint point = NSPointFromCGPoint(Flip(window_location).ToCGPoint()); 84 return [NSEvent mouseEventWithType:type 85 location:point 86 modifierFlags:modifier_flags 87 timestamp:0 88 windowNumber:[test_window() windowNumber] 89 context:nil 90 eventNumber:0 91 clickCount:0 92 pressure:1.0]; 93 } 94 95 NSEvent* TestScrollEvent(const gfx::Point& window_location, 96 int32_t delta_x, 97 int32_t delta_y) { 98 SwizzleTestWindow(); 99 base::ScopedCFTypeRef<CGEventRef> scroll( 100 CGEventCreateScrollWheelEvent(NULL, 101 kCGScrollEventUnitLine, 102 2, 103 delta_y, 104 delta_x)); 105 // CGEvents are always in global display coordinates. These are like screen 106 // coordinates, but flipped. But first the point needs to be converted out 107 // of window coordinates (which also requires flipping). 108 NSPoint window_point = 109 NSPointFromCGPoint(Flip(window_location).ToCGPoint()); 110 NSPoint screen_point = [test_window() convertBaseToScreen:window_point]; 111 CGFloat primary_screen_height = 112 NSHeight([[[NSScreen screens] objectAtIndex:0] frame]); 113 screen_point.y = primary_screen_height - screen_point.y; 114 CGEventSetLocation(scroll, NSPointToCGPoint(screen_point)); 115 return [NSEvent eventWithCGEvent:scroll]; 116 } 117 118 private: 119 scoped_ptr<base::mac::ScopedObjCClassSwizzler> swizzler_; 120 121 DISALLOW_COPY_AND_ASSIGN(EventsMacTest); 122 }; 123 124 } // namespace 125 126 TEST_F(EventsMacTest, EventFlagsFromNative) { 127 // Left click. 128 NSEvent* left = cocoa_test_event_utils::MouseEventWithType(NSLeftMouseUp, 0); 129 EXPECT_EQ(EF_LEFT_MOUSE_BUTTON, EventFlagsFromNative(left)); 130 131 // Right click. 132 NSEvent* right = cocoa_test_event_utils::MouseEventWithType(NSRightMouseUp, 133 0); 134 EXPECT_EQ(EF_RIGHT_MOUSE_BUTTON, EventFlagsFromNative(right)); 135 136 // Middle click. 137 NSEvent* middle = cocoa_test_event_utils::MouseEventWithType(NSOtherMouseUp, 138 0); 139 EXPECT_EQ(EF_MIDDLE_MOUSE_BUTTON, EventFlagsFromNative(middle)); 140 141 // Caps + Left 142 NSEvent* caps = cocoa_test_event_utils::MouseEventWithType( 143 NSLeftMouseUp, NSAlphaShiftKeyMask); 144 EXPECT_EQ(EF_LEFT_MOUSE_BUTTON | EF_CAPS_LOCK_DOWN, 145 EventFlagsFromNative(caps)); 146 147 // Shift + Left 148 NSEvent* shift = cocoa_test_event_utils::MouseEventWithType(NSLeftMouseUp, 149 NSShiftKeyMask); 150 EXPECT_EQ(EF_LEFT_MOUSE_BUTTON | EF_SHIFT_DOWN, EventFlagsFromNative(shift)); 151 152 // Ctrl + Left 153 NSEvent* ctrl = cocoa_test_event_utils::MouseEventWithType(NSLeftMouseUp, 154 NSControlKeyMask); 155 EXPECT_EQ(EF_LEFT_MOUSE_BUTTON | EF_CONTROL_DOWN, EventFlagsFromNative(ctrl)); 156 157 // Alt + Left 158 NSEvent* alt = cocoa_test_event_utils::MouseEventWithType(NSLeftMouseUp, 159 NSAlternateKeyMask); 160 EXPECT_EQ(EF_LEFT_MOUSE_BUTTON | EF_ALT_DOWN, EventFlagsFromNative(alt)); 161 162 // Cmd + Left 163 NSEvent* cmd = cocoa_test_event_utils::MouseEventWithType(NSLeftMouseUp, 164 NSCommandKeyMask); 165 EXPECT_EQ(EF_LEFT_MOUSE_BUTTON | EF_COMMAND_DOWN, EventFlagsFromNative(cmd)); 166 167 // Shift + Ctrl + Left 168 NSEvent* shiftctrl = cocoa_test_event_utils::MouseEventWithType( 169 NSLeftMouseUp, NSShiftKeyMask | NSControlKeyMask); 170 EXPECT_EQ(EF_LEFT_MOUSE_BUTTON | EF_SHIFT_DOWN | EF_CONTROL_DOWN, 171 EventFlagsFromNative(shiftctrl)); 172 173 // Cmd + Alt + Right 174 NSEvent* cmdalt = cocoa_test_event_utils::MouseEventWithType( 175 NSLeftMouseUp, NSCommandKeyMask | NSAlternateKeyMask); 176 EXPECT_EQ(EF_LEFT_MOUSE_BUTTON | EF_COMMAND_DOWN | EF_ALT_DOWN, 177 EventFlagsFromNative(cmdalt)); 178 } 179 180 // Tests mouse button presses and mouse wheel events. 181 TEST_F(EventsMacTest, ButtonEvents) { 182 gfx::Point location(5, 10); 183 gfx::Vector2d offset; 184 185 NSEvent* event = TestMouseEvent(NSLeftMouseDown, location, 0); 186 EXPECT_EQ(ui::ET_MOUSE_PRESSED, ui::EventTypeFromNative(event)); 187 EXPECT_EQ(ui::EF_LEFT_MOUSE_BUTTON, ui::EventFlagsFromNative(event)); 188 EXPECT_EQ(location, ui::EventLocationFromNative(event)); 189 190 SwizzleMiddleMouseButton(); 191 event = TestMouseEvent(NSOtherMouseDown, location, NSShiftKeyMask); 192 EXPECT_EQ(ui::ET_MOUSE_PRESSED, ui::EventTypeFromNative(event)); 193 EXPECT_EQ(ui::EF_MIDDLE_MOUSE_BUTTON | ui::EF_SHIFT_DOWN, 194 ui::EventFlagsFromNative(event)); 195 EXPECT_EQ(location, ui::EventLocationFromNative(event)); 196 ClearSwizzle(); 197 198 event = TestMouseEvent(NSRightMouseUp, location, 0); 199 EXPECT_EQ(ui::ET_MOUSE_RELEASED, ui::EventTypeFromNative(event)); 200 EXPECT_EQ(ui::EF_RIGHT_MOUSE_BUTTON, ui::EventFlagsFromNative(event)); 201 EXPECT_EQ(location, ui::EventLocationFromNative(event)); 202 203 // Scroll up. 204 event = TestScrollEvent(location, 0, 1); 205 EXPECT_EQ(ui::ET_MOUSEWHEEL, ui::EventTypeFromNative(event)); 206 EXPECT_EQ(0, ui::EventFlagsFromNative(event)); 207 EXPECT_EQ(location.ToString(), ui::EventLocationFromNative(event).ToString()); 208 offset = ui::GetMouseWheelOffset(event); 209 EXPECT_GT(offset.y(), 0); 210 EXPECT_EQ(0, offset.x()); 211 ClearSwizzle(); 212 213 // Scroll down. 214 event = TestScrollEvent(location, 0, -1); 215 EXPECT_EQ(ui::ET_MOUSEWHEEL, ui::EventTypeFromNative(event)); 216 EXPECT_EQ(0, ui::EventFlagsFromNative(event)); 217 EXPECT_EQ(location, ui::EventLocationFromNative(event)); 218 offset = ui::GetMouseWheelOffset(event); 219 EXPECT_LT(offset.y(), 0); 220 EXPECT_EQ(0, offset.x()); 221 ClearSwizzle(); 222 223 // Scroll left. 224 event = TestScrollEvent(location, 1, 0); 225 EXPECT_EQ(ui::ET_MOUSEWHEEL, ui::EventTypeFromNative(event)); 226 EXPECT_EQ(0, ui::EventFlagsFromNative(event)); 227 EXPECT_EQ(location, ui::EventLocationFromNative(event)); 228 offset = ui::GetMouseWheelOffset(event); 229 EXPECT_EQ(0, offset.y()); 230 EXPECT_GT(offset.x(), 0); 231 ClearSwizzle(); 232 233 // Scroll right. 234 event = TestScrollEvent(location, -1, 0); 235 EXPECT_EQ(ui::ET_MOUSEWHEEL, ui::EventTypeFromNative(event)); 236 EXPECT_EQ(0, ui::EventFlagsFromNative(event)); 237 EXPECT_EQ(location, ui::EventLocationFromNative(event)); 238 offset = ui::GetMouseWheelOffset(event); 239 EXPECT_EQ(0, offset.y()); 240 EXPECT_LT(offset.x(), 0); 241 ClearSwizzle(); 242 } 243 244 // Test correct location when the window has a native titlebar. 245 TEST_F(EventsMacTest, NativeTitlebarEventLocation) { 246 gfx::Point location(5, 10); 247 NSUInteger style_mask = NSTitledWindowMask | NSClosableWindowMask | 248 NSMiniaturizableWindowMask | NSResizableWindowMask; 249 250 // First check that the window provided by ui::CocoaTest is how we think. 251 DCHECK_EQ(NSBorderlessWindowMask, [test_window() styleMask]); 252 [test_window() setStyleMask:style_mask]; 253 DCHECK_EQ(style_mask, [test_window() styleMask]); 254 255 // EventLocationFromNative should behave the same as the ButtonEvents test. 256 NSEvent* event = TestMouseEvent(NSLeftMouseDown, location, 0); 257 EXPECT_EQ(ui::ET_MOUSE_PRESSED, ui::EventTypeFromNative(event)); 258 EXPECT_EQ(ui::EF_LEFT_MOUSE_BUTTON, ui::EventFlagsFromNative(event)); 259 EXPECT_EQ(location, ui::EventLocationFromNative(event)); 260 261 // And be explicit, to ensure the test doesn't depend on some property of the 262 // test harness. The change to the frame rect could be OS-specfic, so set it 263 // to a known value. 264 const CGFloat kTestHeight = 400; 265 NSRect content_rect = NSMakeRect(0, 0, 600, kTestHeight); 266 NSRect frame_rect = [test_window() frameRectForContentRect:content_rect]; 267 [test_window() setFrame:frame_rect display:YES]; 268 event = [NSEvent mouseEventWithType:NSLeftMouseDown 269 location:NSMakePoint(0, 0) // Bottom-left corner. 270 modifierFlags:0 271 timestamp:0 272 windowNumber:[test_window() windowNumber] 273 context:nil 274 eventNumber:0 275 clickCount:0 276 pressure:1.0]; 277 // Bottom-left corner should be flipped. 278 EXPECT_EQ(gfx::Point(0, kTestHeight), ui::EventLocationFromNative(event)); 279 280 // Removing the border, and sending the same event should move it down in the 281 // toolkit-views coordinate system. 282 int height_change = NSHeight(frame_rect) - kTestHeight; 283 EXPECT_GT(height_change, 0); 284 [test_window() setStyleMask:NSBorderlessWindowMask]; 285 [test_window() setFrame:frame_rect display:YES]; 286 EXPECT_EQ(gfx::Point(0, kTestHeight + height_change), 287 ui::EventLocationFromNative(event)); 288 } 289 290 // Testing for ui::EventTypeFromNative() not covered by ButtonEvents. 291 TEST_F(EventsMacTest, EventTypeFromNative) { 292 NSEvent* event = cocoa_test_event_utils::KeyEventWithType(NSKeyDown, 0); 293 EXPECT_EQ(ui::ET_KEY_PRESSED, ui::EventTypeFromNative(event)); 294 295 event = cocoa_test_event_utils::KeyEventWithType(NSKeyUp, 0); 296 EXPECT_EQ(ui::ET_KEY_RELEASED, ui::EventTypeFromNative(event)); 297 298 event = cocoa_test_event_utils::MouseEventWithType(NSLeftMouseDragged, 0); 299 EXPECT_EQ(ui::ET_MOUSE_DRAGGED, ui::EventTypeFromNative(event)); 300 event = cocoa_test_event_utils::MouseEventWithType(NSRightMouseDragged, 0); 301 EXPECT_EQ(ui::ET_MOUSE_DRAGGED, ui::EventTypeFromNative(event)); 302 event = cocoa_test_event_utils::MouseEventWithType(NSOtherMouseDragged, 0); 303 EXPECT_EQ(ui::ET_MOUSE_DRAGGED, ui::EventTypeFromNative(event)); 304 305 event = cocoa_test_event_utils::MouseEventWithType(NSMouseMoved, 0); 306 EXPECT_EQ(ui::ET_MOUSE_MOVED, ui::EventTypeFromNative(event)); 307 308 event = cocoa_test_event_utils::EnterExitEventWithType(NSMouseEntered); 309 EXPECT_EQ(ui::ET_MOUSE_ENTERED, ui::EventTypeFromNative(event)); 310 event = cocoa_test_event_utils::EnterExitEventWithType(NSMouseExited); 311 EXPECT_EQ(ui::ET_MOUSE_EXITED, ui::EventTypeFromNative(event)); 312 } 313 314 } // namespace ui 315