#import <Cocoa/Cocoa.h>
+#import "base/mac/foundation_util.h"
+#import "base/mac/mac_util.h"
+#import "base/mac/sdk_forward_declarations.h"
#include "base/memory/scoped_ptr.h"
+#include "base/message_loop/message_loop.h"
+#include "base/run_loop.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#import "testing/gtest_mac.h"
#include "ui/views/ime/input_method.h"
#include "ui/views/view.h"
#include "ui/views/widget/native_widget_mac.h"
+#include "ui/views/widget/root_view.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_observer.h"
} // namespace
+@interface NativeWidgetMacNotificationWaiter : NSObject {
+ @private
+ scoped_ptr<base::RunLoop> runLoop_;
+ base::scoped_nsobject<NSWindow> window_;
+ int enterCount_;
+ int exitCount_;
+ int targetEnterCount_;
+ int targetExitCount_;
+}
+
+@property(readonly, nonatomic) int enterCount;
+@property(readonly, nonatomic) int exitCount;
+
+// Initialize for the given window and start tracking notifications.
+- (id)initWithWindow:(NSWindow*)window;
+
+// Keep spinning a run loop until the enter and exit counts match.
+- (void)waitForEnterCount:(int)enterCount exitCount:(int)exitCount;
+
+// private:
+// Exit the RunLoop if there is one and the counts being tracked match.
+- (void)maybeQuitForChangedArg:(int*)changedArg;
+
+- (void)onEnter:(NSNotification*)notification;
+- (void)onExit:(NSNotification*)notification;
+
+@end
+
+@implementation NativeWidgetMacNotificationWaiter
+
+@synthesize enterCount = enterCount_;
+@synthesize exitCount = exitCount_;
+
+- (id)initWithWindow:(NSWindow*)window {
+ if ((self = [super init])) {
+ window_.reset([window retain]);
+ NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter];
+ [defaultCenter addObserver:self
+ selector:@selector(onEnter:)
+ name:NSWindowDidEnterFullScreenNotification
+ object:window];
+ [defaultCenter addObserver:self
+ selector:@selector(onExit:)
+ name:NSWindowDidExitFullScreenNotification
+ object:window];
+ }
+ return self;
+}
+
+- (void)dealloc {
+ DCHECK(!runLoop_);
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ [super dealloc];
+}
+
+- (void)waitForEnterCount:(int)enterCount exitCount:(int)exitCount {
+ if (enterCount_ >= enterCount && exitCount_ >= exitCount)
+ return;
+
+ targetEnterCount_ = enterCount;
+ targetExitCount_ = exitCount;
+ runLoop_.reset(new base::RunLoop);
+ runLoop_->Run();
+ runLoop_.reset();
+}
+
+- (void)maybeQuitForChangedArg:(int*)changedArg {
+ ++*changedArg;
+ if (!runLoop_)
+ return;
+
+ if (enterCount_ >= targetEnterCount_ && exitCount_ >= targetExitCount_)
+ runLoop_->Quit();
+}
+
+- (void)onEnter:(NSNotification*)notification {
+ [self maybeQuitForChangedArg:&enterCount_];
+}
+
+- (void)onExit:(NSNotification*)notification {
+ [self maybeQuitForChangedArg:&exitCount_];
+}
+
+@end
+
+// Class to override -[NSWindow toggleFullScreen:] to a no-op. This simulates
+// NSWindow's behavior when attempting to toggle fullscreen state again, when
+// the last attempt failed but Cocoa has not yet sent
+// windowDidFailToEnterFullScreen:.
+@interface BridgedNativeWidgetTestFullScreenWindow : NSWindow {
+ @private
+ int ignoredToggleFullScreenCount_;
+}
+@property(readonly, nonatomic) int ignoredToggleFullScreenCount;
+@end
+
+@implementation BridgedNativeWidgetTestFullScreenWindow
+
+@synthesize ignoredToggleFullScreenCount = ignoredToggleFullScreenCount_;
+
+- (void)toggleFullScreen:(id)sender {
+ ++ignoredToggleFullScreenCount_;
+}
+
+@end
+
namespace views {
namespace test {
}
// internal::NativeWidgetPrivate:
- virtual void InitNativeWidget(const Widget::InitParams& params) OVERRIDE {
+ virtual void InitNativeWidget(const Widget::InitParams& params) override {
ownership_ = params.ownership;
// Usually the bridge gets initialized here. It is skipped to run extra
delegate()->OnNativeWidgetCreated(true);
}
- virtual void ReorderNativeViews() OVERRIDE {
+ virtual void ReorderNativeViews() override {
// Called via Widget::Init to set the content view. No-op in these tests.
}
}
// Overridden from testing::Test:
- virtual void SetUp() OVERRIDE {
+ virtual void SetUp() override {
ui::CocoaTest::SetUp();
Widget::InitParams params;
std::string GetText();
// testing::Test:
- virtual void SetUp() OVERRIDE;
- virtual void TearDown() OVERRIDE;
+ virtual void SetUp() override;
+ virtual void TearDown() override;
protected:
- // TODO(tapted): Make this a EventCountView from widget_unittest.cc.
scoped_ptr<views::View> view_;
scoped_ptr<BridgedNativeWidget> bridge_;
BridgedContentView* ns_view_; // Weak. Owned by bridge_.
void BridgedNativeWidgetTest::SetUp() {
BridgedNativeWidgetTestBase::SetUp();
- view_.reset(new views::View);
+ view_.reset(new views::internal::RootView(widget_.get()));
base::scoped_nsobject<NSWindow> window([test_window() retain]);
+ // BridgedNativeWidget expects to be initialized with a hidden (deferred)
+ // window.
+ [window orderOut:nil];
EXPECT_FALSE([window delegate]);
bridge()->Init(window, Widget::InitParams());
bridge()->SetRootView(view_.get());
ns_view_ = bridge()->ns_view();
+ // Pretend it has been shown via NativeWidgetMac::Show().
+ [window orderFront:nil];
[test_window() makePretendKeyWindowAndSetFirstResponder:bridge()->ns_view()];
}
[[NSWindow alloc] initWithContentRect:NSMakeRect(50, 50, 400, 300)
styleMask:NSBorderlessWindowMask
backing:NSBackingStoreBuffered
- defer:NO]);
+ defer:YES]);
[child_window setReleasedWhenClosed:NO]; // Owned by scoped_nsobject.
EXPECT_FALSE([child_window parentWindow]);
EXPECT_EQ_RANGE(NSMakeRange(0, 0), [ns_view_ selectedRange]);
}
+// Tests for correct fullscreen tracking, regardless of whether it is initiated
+// by the Widget code or elsewhere (e.g. by the user).
+TEST_F(BridgedNativeWidgetTest, FullscreenSynchronousState) {
+ EXPECT_FALSE(widget_->IsFullscreen());
+ if (base::mac::IsOSSnowLeopard())
+ return;
+
+ // Allow user-initiated fullscreen changes on the Window.
+ [test_window()
+ setCollectionBehavior:[test_window() collectionBehavior] |
+ NSWindowCollectionBehaviorFullScreenPrimary];
+
+ base::scoped_nsobject<NativeWidgetMacNotificationWaiter> waiter(
+ [[NativeWidgetMacNotificationWaiter alloc] initWithWindow:test_window()]);
+ const gfx::Rect restored_bounds = widget_->GetRestoredBounds();
+
+ // Simulate a user-initiated fullscreen. Note trying to to this again before
+ // spinning a runloop will cause Cocoa to emit text to stdio and ignore it.
+ [test_window() toggleFullScreen:nil];
+ EXPECT_TRUE(widget_->IsFullscreen());
+ EXPECT_EQ(restored_bounds, widget_->GetRestoredBounds());
+
+ // Note there's now an animation running. While that's happening, toggling the
+ // state should work as expected, but do "nothing".
+ widget_->SetFullscreen(false);
+ EXPECT_FALSE(widget_->IsFullscreen());
+ EXPECT_EQ(restored_bounds, widget_->GetRestoredBounds());
+ widget_->SetFullscreen(false); // Same request - should no-op.
+ EXPECT_FALSE(widget_->IsFullscreen());
+ EXPECT_EQ(restored_bounds, widget_->GetRestoredBounds());
+
+ widget_->SetFullscreen(true);
+ EXPECT_TRUE(widget_->IsFullscreen());
+ EXPECT_EQ(restored_bounds, widget_->GetRestoredBounds());
+
+ // Always finish out of fullscreen. Otherwise there are 4 NSWindow objects
+ // that Cocoa creates which don't close themselves and will be seen by the Mac
+ // test harness on teardown. Note that the test harness will be waiting until
+ // all animations complete, since these temporary animation windows will not
+ // be removed from the window list until they do.
+ widget_->SetFullscreen(false);
+ EXPECT_EQ(restored_bounds, widget_->GetRestoredBounds());
+
+ // Now we must wait for the notifications. Since, if the widget is torn down,
+ // the NSWindowDelegate is removed, and the pending request to take out of
+ // fullscreen is lost. Since a message loop has not yet spun up in this test
+ // we can reliably say there will be one enter and one exit, despite all the
+ // toggling above.
+ base::MessageLoopForUI message_loop;
+ [waiter waitForEnterCount:1 exitCount:1];
+ EXPECT_EQ(restored_bounds, widget_->GetRestoredBounds());
+}
+
+// Test fullscreen without overlapping calls and without changing collection
+// behavior on the test window.
+TEST_F(BridgedNativeWidgetTest, FullscreenEnterAndExit) {
+ base::MessageLoopForUI message_loop;
+ base::scoped_nsobject<NativeWidgetMacNotificationWaiter> waiter(
+ [[NativeWidgetMacNotificationWaiter alloc] initWithWindow:test_window()]);
+
+ EXPECT_FALSE(widget_->IsFullscreen());
+ const gfx::Rect restored_bounds = widget_->GetRestoredBounds();
+ EXPECT_FALSE(restored_bounds.IsEmpty());
+
+ // Ensure this works without having to change collection behavior as for the
+ // test above.
+ widget_->SetFullscreen(true);
+ if (base::mac::IsOSSnowLeopard()) {
+ // On Snow Leopard, SetFullscreen() isn't implemented. But shouldn't crash.
+ EXPECT_FALSE(widget_->IsFullscreen());
+ return;
+ }
+
+ EXPECT_TRUE(widget_->IsFullscreen());
+ EXPECT_EQ(restored_bounds, widget_->GetRestoredBounds());
+
+ // Should be zero until the runloop spins.
+ EXPECT_EQ(0, [waiter enterCount]);
+ [waiter waitForEnterCount:1 exitCount:0];
+
+ // Verify it hasn't exceeded.
+ EXPECT_EQ(1, [waiter enterCount]);
+ EXPECT_EQ(0, [waiter exitCount]);
+ EXPECT_TRUE(widget_->IsFullscreen());
+ EXPECT_EQ(restored_bounds, widget_->GetRestoredBounds());
+
+ widget_->SetFullscreen(false);
+ EXPECT_FALSE(widget_->IsFullscreen());
+ EXPECT_EQ(restored_bounds, widget_->GetRestoredBounds());
+
+ [waiter waitForEnterCount:1 exitCount:1];
+ EXPECT_EQ(1, [waiter enterCount]);
+ EXPECT_EQ(1, [waiter exitCount]);
+ EXPECT_EQ(restored_bounds, widget_->GetRestoredBounds());
+}
+
+typedef BridgedNativeWidgetTestBase BridgedNativeWidgetSimulateFullscreenTest;
+
+// Simulate the notifications that AppKit would send out if a fullscreen
+// operation begins, and then fails and must abort. This notification sequence
+// was determined by posting delayed tasks to toggle fullscreen state and then
+// mashing Ctrl+Left/Right to keep OSX in a transition between Spaces to cause
+// the fullscreen transition to fail.
+TEST_F(BridgedNativeWidgetSimulateFullscreenTest, FailToEnterAndExit) {
+ if (base::mac::IsOSSnowLeopard())
+ return;
+
+ base::scoped_nsobject<NSWindow> owned_window(
+ [[BridgedNativeWidgetTestFullScreenWindow alloc]
+ initWithContentRect:NSMakeRect(50, 50, 400, 300)
+ styleMask:NSBorderlessWindowMask
+ backing:NSBackingStoreBuffered
+ defer:YES]);
+ [owned_window setReleasedWhenClosed:NO]; // Owned by scoped_nsobject.
+ bridge()->Init(owned_window, Widget::InitParams()); // Transfers ownership.
+
+ BridgedNativeWidgetTestFullScreenWindow* window =
+ base::mac::ObjCCastStrict<BridgedNativeWidgetTestFullScreenWindow>(
+ widget_->GetNativeWindow());
+ widget_->Show();
+
+ NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
+
+ EXPECT_FALSE(bridge()->target_fullscreen_state());
+
+ // Simulate an initial toggleFullScreen: (user- or Widget-initiated).
+ [center postNotificationName:NSWindowWillEnterFullScreenNotification
+ object:window];
+
+ // On a failure, Cocoa starts by sending an unexpected *exit* fullscreen, and
+ // BridgedNativeWidget will think it's just a delayed transition and try to go
+ // back into fullscreen but get ignored by Cocoa.
+ EXPECT_EQ(0, [window ignoredToggleFullScreenCount]);
+ EXPECT_TRUE(bridge()->target_fullscreen_state());
+ [center postNotificationName:NSWindowDidExitFullScreenNotification
+ object:window];
+ EXPECT_EQ(1, [window ignoredToggleFullScreenCount]);
+ EXPECT_FALSE(bridge()->target_fullscreen_state());
+
+ // Cocoa follows up with a failure message sent to the NSWindowDelegate (there
+ // is no equivalent notification for failure). Called via id so that this
+ // compiles on 10.6.
+ id window_delegate = [window delegate];
+ [window_delegate windowDidFailToEnterFullScreen:window];
+ EXPECT_FALSE(bridge()->target_fullscreen_state());
+
+ // Now perform a successful fullscreen operation.
+ [center postNotificationName:NSWindowWillEnterFullScreenNotification
+ object:window];
+ EXPECT_TRUE(bridge()->target_fullscreen_state());
+ [center postNotificationName:NSWindowDidEnterFullScreenNotification
+ object:window];
+ EXPECT_TRUE(bridge()->target_fullscreen_state());
+
+ // And try to get out.
+ [center postNotificationName:NSWindowWillExitFullScreenNotification
+ object:window];
+ EXPECT_FALSE(bridge()->target_fullscreen_state());
+
+ // On a failure, Cocoa sends a failure message, but then just dumps the window
+ // out of fullscreen anyway (in that order).
+ [window_delegate windowDidFailToExitFullScreen:window];
+ EXPECT_FALSE(bridge()->target_fullscreen_state());
+ [center postNotificationName:NSWindowDidExitFullScreenNotification
+ object:window];
+ EXPECT_EQ(1, [window ignoredToggleFullScreenCount]); // No change.
+ EXPECT_FALSE(bridge()->target_fullscreen_state());
+
+ widget_->CloseNow();
+}
+
} // namespace test
} // namespace views