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