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 "ui/views/widget/native_widget_mac.h"
7 #import <Cocoa/Cocoa.h>
9 #import "base/mac/scoped_nsobject.h"
10 #include "base/run_loop.h"
11 #include "base/strings/utf_string_conversions.h"
12 #include "base/strings/sys_string_conversions.h"
13 #import "testing/gtest_mac.h"
14 #import "ui/events/test/cocoa_test_event_utils.h"
15 #include "ui/events/test/event_generator.h"
16 #import "ui/gfx/mac/coordinate_conversion.h"
17 #import "ui/views/cocoa/bridged_native_widget.h"
18 #include "ui/views/controls/button/label_button.h"
19 #include "ui/views/controls/label.h"
20 #include "ui/views/native_cursor.h"
21 #include "ui/views/test/test_widget_observer.h"
22 #include "ui/views/test/widget_test.h"
23 #include "ui/views/widget/native_widget_private.h"
28 // Tests for parts of NativeWidgetMac not covered by BridgedNativeWidget, which
29 // need access to Cocoa APIs.
30 typedef WidgetTest NativeWidgetMacTest;
32 class WidgetChangeObserver : public TestWidgetObserver {
34 WidgetChangeObserver(Widget* widget)
35 : TestWidgetObserver(widget),
36 gained_visible_count_(0),
37 lost_visible_count_(0) {}
39 int gained_visible_count() const { return gained_visible_count_; }
40 int lost_visible_count() const { return lost_visible_count_; }
44 void OnWidgetVisibilityChanged(Widget* widget,
45 bool visible) override {
46 ++(visible ? gained_visible_count_ : lost_visible_count_);
49 int gained_visible_count_;
50 int lost_visible_count_;
52 DISALLOW_COPY_AND_ASSIGN(WidgetChangeObserver);
55 // Test visibility states triggered externally.
56 TEST_F(NativeWidgetMacTest, HideAndShowExternally) {
57 Widget* widget = CreateTopLevelPlatformWidget();
58 NSWindow* ns_window = widget->GetNativeWindow();
59 WidgetChangeObserver observer(widget);
61 // Should initially be hidden.
62 EXPECT_FALSE(widget->IsVisible());
63 EXPECT_FALSE([ns_window isVisible]);
64 EXPECT_EQ(0, observer.gained_visible_count());
65 EXPECT_EQ(0, observer.lost_visible_count());
68 EXPECT_TRUE(widget->IsVisible());
69 EXPECT_TRUE([ns_window isVisible]);
70 EXPECT_EQ(1, observer.gained_visible_count());
71 EXPECT_EQ(0, observer.lost_visible_count());
74 EXPECT_FALSE(widget->IsVisible());
75 EXPECT_FALSE([ns_window isVisible]);
76 EXPECT_EQ(1, observer.gained_visible_count());
77 EXPECT_EQ(1, observer.lost_visible_count());
80 EXPECT_TRUE(widget->IsVisible());
81 EXPECT_TRUE([ns_window isVisible]);
82 EXPECT_EQ(2, observer.gained_visible_count());
83 EXPECT_EQ(1, observer.lost_visible_count());
85 // Test when hiding individual windows.
86 [ns_window orderOut:nil];
87 EXPECT_FALSE(widget->IsVisible());
88 EXPECT_FALSE([ns_window isVisible]);
89 EXPECT_EQ(2, observer.gained_visible_count());
90 EXPECT_EQ(2, observer.lost_visible_count());
92 [ns_window orderFront:nil];
93 EXPECT_TRUE(widget->IsVisible());
94 EXPECT_TRUE([ns_window isVisible]);
95 EXPECT_EQ(3, observer.gained_visible_count());
96 EXPECT_EQ(2, observer.lost_visible_count());
98 // Test when hiding the entire application. This doesn't send an orderOut:
101 // When the activation policy is NSApplicationActivationPolicyRegular, the
102 // calls via NSApp are asynchronous, and the run loop needs to be flushed.
103 // With NSApplicationActivationPolicyProhibited, the following RunUntilIdle
104 // calls are superfluous, but don't hurt.
105 base::RunLoop().RunUntilIdle();
106 EXPECT_FALSE(widget->IsVisible());
107 EXPECT_FALSE([ns_window isVisible]);
108 EXPECT_EQ(3, observer.gained_visible_count());
109 EXPECT_EQ(3, observer.lost_visible_count());
111 [NSApp unhideWithoutActivation];
112 base::RunLoop().RunUntilIdle();
113 EXPECT_TRUE(widget->IsVisible());
114 EXPECT_TRUE([ns_window isVisible]);
115 EXPECT_EQ(4, observer.gained_visible_count());
116 EXPECT_EQ(3, observer.lost_visible_count());
118 // Hide again to test unhiding with an activation.
120 base::RunLoop().RunUntilIdle();
121 EXPECT_EQ(4, observer.lost_visible_count());
123 base::RunLoop().RunUntilIdle();
124 EXPECT_EQ(5, observer.gained_visible_count());
126 // Hide again to test makeKeyAndOrderFront:.
127 [ns_window orderOut:nil];
128 EXPECT_FALSE(widget->IsVisible());
129 EXPECT_FALSE([ns_window isVisible]);
130 EXPECT_EQ(5, observer.gained_visible_count());
131 EXPECT_EQ(5, observer.lost_visible_count());
133 [ns_window makeKeyAndOrderFront:nil];
134 EXPECT_TRUE(widget->IsVisible());
135 EXPECT_TRUE([ns_window isVisible]);
136 EXPECT_EQ(6, observer.gained_visible_count());
137 EXPECT_EQ(5, observer.lost_visible_count());
139 // No change when closing.
141 EXPECT_EQ(5, observer.lost_visible_count());
142 EXPECT_EQ(6, observer.gained_visible_count());
145 // A view that counts calls to OnPaint().
146 class PaintCountView : public View {
148 PaintCountView() : paint_count_(0) {
149 SetBounds(0, 0, 100, 100);
153 void OnPaint(gfx::Canvas* canvas) override {
154 EXPECT_TRUE(GetWidget()->IsVisible());
158 int paint_count() { return paint_count_; }
163 DISALLOW_COPY_AND_ASSIGN(PaintCountView);
166 // Test minimized states triggered externally, implied visibility and restored
167 // bounds whilst minimized.
168 TEST_F(NativeWidgetMacTest, MiniaturizeExternally) {
169 Widget* widget = new Widget;
170 Widget::InitParams init_params(Widget::InitParams::TYPE_WINDOW);
171 // Make the layer not drawn, so that calls to paint can be observed
173 init_params.layer_type = ui::LAYER_NOT_DRAWN;
174 widget->Init(init_params);
176 PaintCountView* view = new PaintCountView();
177 widget->GetContentsView()->AddChildView(view);
178 NSWindow* ns_window = widget->GetNativeWindow();
179 WidgetChangeObserver observer(widget);
181 widget->SetBounds(gfx::Rect(100, 100, 300, 300));
183 EXPECT_TRUE(view->IsDrawn());
184 EXPECT_EQ(0, view->paint_count());
187 EXPECT_EQ(1, observer.gained_visible_count());
188 EXPECT_EQ(0, observer.lost_visible_count());
189 const gfx::Rect restored_bounds = widget->GetRestoredBounds();
190 EXPECT_FALSE(restored_bounds.IsEmpty());
191 EXPECT_FALSE(widget->IsMinimized());
192 EXPECT_TRUE(widget->IsVisible());
194 // Showing should paint.
195 EXPECT_EQ(1, view->paint_count());
197 // First try performMiniaturize:, which requires a minimize button. Note that
198 // Cocoa just blocks the UI thread during the animation, so no need to do
199 // anything fancy to wait for it finish.
200 [ns_window performMiniaturize:nil];
202 EXPECT_TRUE(widget->IsMinimized());
203 EXPECT_FALSE(widget->IsVisible()); // Minimizing also makes things invisible.
204 EXPECT_EQ(1, observer.gained_visible_count());
205 EXPECT_EQ(1, observer.lost_visible_count());
206 EXPECT_EQ(restored_bounds, widget->GetRestoredBounds());
208 // No repaint when minimizing. But note that this is partly due to not calling
209 // [NSView setNeedsDisplay:YES] on the content view. The superview, which is
210 // an NSThemeFrame, would repaint |view| if we had, because the miniaturize
211 // button is highlighted for performMiniaturize.
212 EXPECT_EQ(1, view->paint_count());
214 [ns_window deminiaturize:nil];
216 EXPECT_FALSE(widget->IsMinimized());
217 EXPECT_TRUE(widget->IsVisible());
218 EXPECT_EQ(2, observer.gained_visible_count());
219 EXPECT_EQ(1, observer.lost_visible_count());
220 EXPECT_EQ(restored_bounds, widget->GetRestoredBounds());
222 EXPECT_EQ(2, view->paint_count()); // A single paint when deminiaturizing.
223 EXPECT_FALSE([ns_window isMiniaturized]);
227 EXPECT_TRUE(widget->IsMinimized());
228 EXPECT_TRUE([ns_window isMiniaturized]);
229 EXPECT_EQ(2, observer.gained_visible_count());
230 EXPECT_EQ(2, observer.lost_visible_count());
231 EXPECT_EQ(restored_bounds, widget->GetRestoredBounds());
232 EXPECT_EQ(2, view->paint_count()); // No paint when miniaturizing.
234 widget->Restore(); // If miniaturized, should deminiaturize.
236 EXPECT_FALSE(widget->IsMinimized());
237 EXPECT_FALSE([ns_window isMiniaturized]);
238 EXPECT_EQ(3, observer.gained_visible_count());
239 EXPECT_EQ(2, observer.lost_visible_count());
240 EXPECT_EQ(restored_bounds, widget->GetRestoredBounds());
241 EXPECT_EQ(3, view->paint_count());
243 widget->Restore(); // If not miniaturized, does nothing.
245 EXPECT_FALSE(widget->IsMinimized());
246 EXPECT_FALSE([ns_window isMiniaturized]);
247 EXPECT_EQ(3, observer.gained_visible_count());
248 EXPECT_EQ(2, observer.lost_visible_count());
249 EXPECT_EQ(restored_bounds, widget->GetRestoredBounds());
250 EXPECT_EQ(3, view->paint_count());
254 // Create a widget without a minimize button.
255 widget = CreateTopLevelFramelessPlatformWidget();
256 ns_window = widget->GetNativeWindow();
257 widget->SetBounds(gfx::Rect(100, 100, 300, 300));
259 EXPECT_FALSE(widget->IsMinimized());
261 // This should fail, since performMiniaturize: requires a minimize button.
262 [ns_window performMiniaturize:nil];
263 EXPECT_FALSE(widget->IsMinimized());
265 // But this should work.
267 EXPECT_TRUE(widget->IsMinimized());
269 // Test closing while minimized.
273 // Simple view for the SetCursor test that overrides View::GetCursor().
274 class CursorView : public View {
276 CursorView(int x, NSCursor* cursor) : cursor_(cursor) {
277 SetBounds(x, 0, 100, 300);
281 gfx::NativeCursor GetCursor(const ui::MouseEvent& event) override {
288 DISALLOW_COPY_AND_ASSIGN(CursorView);
291 // Test for Widget::SetCursor(). There is no Widget::GetCursor(), so this uses
292 // -[NSCursor currentCursor] to validate expectations. Note that currentCursor
293 // is just "the top cursor on the application's cursor stack.", which is why it
294 // is safe to use this in a non-interactive UI test with the EventGenerator.
295 TEST_F(NativeWidgetMacTest, SetCursor) {
296 NSCursor* arrow = [NSCursor arrowCursor];
297 NSCursor* hand = GetNativeHandCursor();
298 NSCursor* ibeam = GetNativeIBeamCursor();
300 Widget* widget = CreateTopLevelPlatformWidget();
301 widget->SetBounds(gfx::Rect(0, 0, 300, 300));
302 widget->GetContentsView()->AddChildView(new CursorView(0, hand));
303 widget->GetContentsView()->AddChildView(new CursorView(100, ibeam));
306 // Events used to simulate tracking rectangle updates. These are not passed to
307 // toolkit-views, so it only matters whether they are inside or outside the
309 NSEvent* event_in_content = cocoa_test_event_utils::MouseEventAtPoint(
310 NSMakePoint(100, 100), NSMouseMoved, 0);
311 NSEvent* event_out_of_content = cocoa_test_event_utils::MouseEventAtPoint(
312 NSMakePoint(-50, -50), NSMouseMoved, 0);
314 EXPECT_NE(arrow, hand);
315 EXPECT_NE(arrow, ibeam);
317 // At the start of the test, the cursor stack should be empty.
318 EXPECT_FALSE([NSCursor currentCursor]);
320 // Use an event generator to ask views code to set the cursor. However, note
321 // that this does not cause Cocoa to generate tracking rectangle updates.
322 ui::test::EventGenerator event_generator(GetContext(),
323 widget->GetNativeWindow());
325 // Move the mouse over the first view, then simulate a tracking rectangle
327 event_generator.MoveMouseTo(gfx::Point(50, 50));
328 [widget->GetNativeWindow() cursorUpdate:event_in_content];
329 EXPECT_EQ(hand, [NSCursor currentCursor]);
331 // A tracking rectangle update not in the content area should forward to
332 // the native NSWindow implementation, which sets the arrow cursor.
333 [widget->GetNativeWindow() cursorUpdate:event_out_of_content];
334 EXPECT_EQ(arrow, [NSCursor currentCursor]);
336 // Now move to the second view.
337 event_generator.MoveMouseTo(gfx::Point(150, 50));
338 [widget->GetNativeWindow() cursorUpdate:event_in_content];
339 EXPECT_EQ(ibeam, [NSCursor currentCursor]);
341 // Moving to the third view (but remaining in the content area) should also
342 // forward to the native NSWindow implementation.
343 event_generator.MoveMouseTo(gfx::Point(250, 50));
344 [widget->GetNativeWindow() cursorUpdate:event_in_content];
345 EXPECT_EQ(arrow, [NSCursor currentCursor]);
350 // Tests that an accessibility request from the system makes its way through to
351 // a views::Label filling the window.
352 TEST_F(NativeWidgetMacTest, AccessibilityIntegration) {
353 Widget* widget = CreateTopLevelPlatformWidget();
354 gfx::Rect screen_rect(50, 50, 100, 100);
355 widget->SetBounds(screen_rect);
357 const base::string16 test_string = base::ASCIIToUTF16("Green");
358 views::Label* label = new views::Label(test_string);
359 label->SetBounds(0, 0, 100, 100);
360 widget->GetContentsView()->AddChildView(label);
363 // Accessibility hit tests come in Cocoa screen coordinates.
364 NSRect nsrect = gfx::ScreenRectToNSRect(screen_rect);
365 NSPoint midpoint = NSMakePoint(NSMidX(nsrect), NSMidY(nsrect));
367 id hit = [widget->GetNativeWindow() accessibilityHitTest:midpoint];
368 id title = [hit accessibilityAttributeValue:NSAccessibilityTitleAttribute];
369 EXPECT_NSEQ(title, @"Green");
372 // Tests creating a views::Widget parented off a native NSWindow.
373 TEST_F(NativeWidgetMacTest, NonWidgetParent) {
374 NSRect parent_nsrect = NSMakeRect(100, 100, 300, 200);
375 base::scoped_nsobject<NSWindow> native_parent(
376 [[NSWindow alloc] initWithContentRect:parent_nsrect
377 styleMask:NSBorderlessWindowMask
378 backing:NSBackingStoreBuffered
380 [native_parent setReleasedWhenClosed:NO]; // Owned by scoped_nsobject.
381 [native_parent makeKeyAndOrderFront:nil];
383 // Note: Don't use WidgetTest::CreateChildPlatformWidget because that makes
384 // windows of TYPE_CONTROL which are automatically made visible. But still
385 // mark it as a child to test window positioning.
386 Widget* child = new Widget;
387 Widget::InitParams init_params;
388 init_params.parent = [native_parent contentView];
389 init_params.child = true;
390 child->Init(init_params);
392 TestWidgetObserver child_observer(child);
394 // GetTopLevelNativeWidget() only goes as far as there exists a Widget (i.e.
395 // must stop at |child|.
396 internal::NativeWidgetPrivate* top_level_widget =
397 internal::NativeWidgetPrivate::GetTopLevelNativeWidget(
398 child->GetNativeView());
399 EXPECT_EQ(child, top_level_widget->GetWidget());
401 // To verify the parent, we need to use NativeWidgetMac APIs.
402 BridgedNativeWidget* bridged_native_widget =
403 NativeWidgetMac::GetBridgeForNativeWindow(child->GetNativeWindow());
404 EXPECT_EQ(native_parent, bridged_native_widget->parent()->GetNSWindow());
406 child->SetBounds(gfx::Rect(50, 50, 200, 100));
407 EXPECT_FALSE(child->IsVisible());
408 EXPECT_EQ(0u, [[native_parent childWindows] count]);
411 EXPECT_TRUE(child->IsVisible());
412 EXPECT_EQ(1u, [[native_parent childWindows] count]);
413 EXPECT_EQ(child->GetNativeWindow(),
414 [[native_parent childWindows] objectAtIndex:0]);
415 EXPECT_EQ(native_parent, [child->GetNativeWindow() parentWindow]);
417 // Child should be positioned on screen relative to the parent, but note we
418 // positioned the parent in Cooca coordinates, so we need to convert.
419 gfx::Point parent_origin = gfx::ScreenRectFromNSRect(parent_nsrect).origin();
420 EXPECT_EQ(gfx::Rect(150, parent_origin.y() + 50, 200, 100),
421 child->GetWindowBoundsInScreen());
423 // Closing the parent should close and destroy the child.
424 EXPECT_FALSE(child_observer.widget_closed());
425 [native_parent close];
426 EXPECT_TRUE(child_observer.widget_closed());
428 EXPECT_EQ(0u, [[native_parent childWindows] count]);
431 // Use Native APIs to query the tooltip text that would be shown once the
432 // tooltip delay had elapsed.
433 base::string16 TooltipTextForWidget(Widget* widget) {
434 // For Mac, the actual location doesn't matter, since there is only one native
435 // view and it fills the window. This just assumes the window is at least big
436 // big enough for a constant coordinate to be within it.
437 NSPoint point = NSMakePoint(30, 30);
438 NSView* view = [widget->GetNativeView() hitTest:point];
440 [view view:view stringForToolTip:0 point:point userData:nullptr];
441 return base::SysNSStringToUTF16(text);
444 // Tests tooltips. The test doesn't wait for tooltips to appear. That is, the
445 // test assumes Cocoa calls stringForToolTip: at appropriate times and that,
446 // when a tooltip is already visible, changing it causes an update. These were
447 // tested manually by inserting a base::RunLoop.Run().
448 TEST_F(NativeWidgetMacTest, Tooltips) {
449 Widget* widget = CreateTopLevelPlatformWidget();
450 gfx::Rect screen_rect(50, 50, 100, 100);
451 widget->SetBounds(screen_rect);
453 const base::string16 tooltip_back = base::ASCIIToUTF16("Back");
454 const base::string16 tooltip_front = base::ASCIIToUTF16("Front");
455 const base::string16 long_tooltip(2000, 'W');
457 // Create a nested layout to test corner cases.
458 LabelButton* back = new LabelButton(nullptr, base::string16());
459 back->SetBounds(10, 10, 80, 80);
460 widget->GetContentsView()->AddChildView(back);
463 ui::test::EventGenerator event_generator(GetContext(),
464 widget->GetNativeWindow());
466 // Initially, there should be no tooltip.
467 event_generator.MoveMouseTo(gfx::Point(50, 50));
468 EXPECT_TRUE(TooltipTextForWidget(widget).empty());
470 // Create a new button for the "front", and set the tooltip, but don't add it
471 // to the view hierarchy yet.
472 LabelButton* front = new LabelButton(nullptr, base::string16());
473 front->SetBounds(20, 20, 40, 40);
474 front->SetTooltipText(tooltip_front);
476 // Changing the tooltip text shouldn't require an additional mousemove to take
478 EXPECT_TRUE(TooltipTextForWidget(widget).empty());
479 back->SetTooltipText(tooltip_back);
480 EXPECT_EQ(tooltip_back, TooltipTextForWidget(widget));
482 // Adding a new view under the mouse should also take immediate effect.
483 back->AddChildView(front);
484 EXPECT_EQ(tooltip_front, TooltipTextForWidget(widget));
486 // A long tooltip will be wrapped by Cocoa, but the full string should appear.
487 // Note that render widget hosts clip at 1024 to prevent DOS, but in toolkit-
488 // views the UI is more trusted.
489 front->SetTooltipText(long_tooltip);
490 EXPECT_EQ(long_tooltip, TooltipTextForWidget(widget));
492 // Move the mouse to a different view - tooltip should change.
493 event_generator.MoveMouseTo(gfx::Point(15, 15));
494 EXPECT_EQ(tooltip_back, TooltipTextForWidget(widget));
496 // Move the mouse off of any view, tooltip should clear.
497 event_generator.MoveMouseTo(gfx::Point(5, 5));
498 EXPECT_TRUE(TooltipTextForWidget(widget).empty());