Commit 715cb510 authored by tapted's avatar tapted Committed by Commit bot

MacViews: Implement NativeWidgetMac::SetCursor()

Tracking areas in Cocoa are a horrible mess. 10.5 semi-deprecated
NSWindow cursorRects, and introduced [NSResponder cursorUpdate:]. Both
are supported and they react with each other in strange ways.
Intercepting events for toolkit-views SetCapture seems to further
complicate things.

After numerous dead-ends, this CL implements SetCursor() by overriding
cursorUpdate: on NativeWidgetMac's NSWindow override. This works because
cursorUpdate: messages are forwarded along the responder chain (starting
from varied and somewhat unpredictable starting points), and usually end
up at the NSWindow.

Rather than adding more NSTrackingArea cruft, the one in
BridgedContentView is updated to handle cursor updates. This ensures the
cursor is reset to "normal" whenever it leaves the content area.

Installing a global event monitor also resets the mouse cursor to an
arrow. I tried tracing this with NSObjCMessageLoggingEnabled, but the
reset isn't done with Objective-C. To fix, "remind" AppKit what the
cursor should be after setting capture.

See code comments for all the other traps.

Testing tracking rectangles with simulated events is pretty much a lost
cause, but NativeWidgetMacTest.SetCursor() is added which gets some
coverage by sending an event directly to [NSWindow cursorUpdate:], and
using the ui::test::EventGenerator to forward events to toolkit-views to
actually request new cursors to set.

BUG=454698

Review URL: https://codereview.chromium.org/891003004

Cr-Commit-Position: refs/heads/master@{#314660}
parent 0ae45e68
......@@ -79,13 +79,14 @@ gfx::Point MovePointToWindow(const NSPoint& point,
if ((self = [super initWithFrame:initialFrame])) {
hostedView_ = viewToHost;
trackingArea_.reset(
[[CrTrackingArea alloc] initWithRect:NSZeroRect
options:NSTrackingMouseMoved |
NSTrackingActiveAlways |
NSTrackingInVisibleRect
owner:self
userInfo:nil]);
// Apple's documentation says that NSTrackingActiveAlways is incompatible
// with NSTrackingCursorUpdate, so use NSTrackingActiveInActiveApp.
trackingArea_.reset([[CrTrackingArea alloc]
initWithRect:NSZeroRect
options:NSTrackingMouseMoved | NSTrackingCursorUpdate |
NSTrackingActiveInActiveApp | NSTrackingInVisibleRect
owner:self
userInfo:nil]);
[self addTrackingArea:trackingArea_.get()];
}
return self;
......
......@@ -83,6 +83,9 @@ class VIEWS_EXPORT BridgedNativeWidget : public ui::LayerDelegate,
void SetNativeWindowProperty(const char* key, void* value);
void* GetNativeWindowProperty(const char* key) const;
// Sets the cursor associated with the NSWindow. Retains |cursor|.
void SetCursor(NSCursor* cursor);
// Called internally by the NSWindowDelegate when the window is closing.
void OnWindowWillClose();
......
......@@ -204,6 +204,14 @@ void BridgedNativeWidget::AcquireCapture() {
return; // Capture on hidden windows is disallowed.
mouse_capture_.reset(new CocoaMouseCapture(this));
// Initiating global event capture with addGlobalMonitorForEventsMatchingMask:
// will reset the mouse cursor to an arrow. Asking the window for an update
// here will restore what we want. However, it can sometimes cause the cursor
// to flicker, once, on the initial mouseDown.
// TOOD(tapted): Make this unnecessary by only asking for global mouse capture
// for the cases that need it (e.g. menus, but not drag and drop).
[window_ cursorUpdate:[NSApp currentEvent]];
}
void BridgedNativeWidget::ReleaseCapture() {
......@@ -230,6 +238,10 @@ void* BridgedNativeWidget::GetNativeWindowProperty(const char* name) const {
return [[GetWindowProperties() objectForKey:key] pointerValue];
}
void BridgedNativeWidget::SetCursor(NSCursor* cursor) {
[window_delegate_ setCursor:cursor];
}
void BridgedNativeWidget::OnWindowWillClose() {
if (parent_)
parent_->RemoveChildWindow(this);
......
......@@ -59,4 +59,26 @@
[[self viewsNSWindowDelegate] onWindowOrderChanged:nil];
}
// NSResponder implementation.
- (void)cursorUpdate:(NSEvent*)theEvent {
// The cursor provided by the delegate should only be applied within the
// content area. This is because we rely on the contentView to track the
// mouse cursor and forward cursorUpdate: messages up the responder chain.
// The cursorUpdate: isn't handled in BridgedContentView because views-style
// SetCapture() conflicts with the way tracking events are processed for
// the view during a drag. Since the NSWindow is still in the responder chain
// overriding cursorUpdate: here handles both cases.
if (!NSPointInRect([theEvent locationInWindow], [[self contentView] frame])) {
[super cursorUpdate:theEvent];
return;
}
NSCursor* cursor = [[self viewsNSWindowDelegate] cursor];
if (cursor)
[cursor set];
else
[super cursorUpdate:theEvent];
}
@end
......@@ -7,6 +7,8 @@
#import <Cocoa/Cocoa.h>
#import "base/mac/scoped_nsobject.h"
namespace views {
class NativeWidgetMac;
class BridgedNativeWidget;
......@@ -17,12 +19,17 @@ class BridgedNativeWidget;
@interface ViewsNSWindowDelegate : NSObject<NSWindowDelegate> {
@private
views::BridgedNativeWidget* parent_; // Weak. Owns this.
base::scoped_nsobject<NSCursor> cursor_;
}
// The NativeWidgetMac that created the window this is attached to. Returns
// NULL if not created by NativeWidgetMac.
@property(nonatomic, readonly) views::NativeWidgetMac* nativeWidgetMac;
// If set, the cursor set in -[NSResponder updateCursor:] when the window is
// reached along the responder chain.
@property(retain, nonatomic) NSCursor* cursor;
// Initialize with the given |parent|.
- (id)initWithBridgedNativeWidget:(views::BridgedNativeWidget*)parent;
......
......@@ -23,6 +23,18 @@
return parent_->native_widget_mac();
}
- (NSCursor*)cursor {
return cursor_.get();
}
- (void)setCursor:(NSCursor*)newCursor {
if (cursor_.get() == newCursor)
return;
cursor_.reset([newCursor retain]);
[parent_->ns_window() resetCursorRects];
}
- (void)onWindowOrderWillChange:(NSWindowOrderingMode)orderingMode {
parent_->OnVisibilityChangedTo(orderingMode != NSWindowOut);
}
......
......@@ -471,7 +471,8 @@ void NativeWidgetMac::SchedulePaintInRect(const gfx::Rect& rect) {
}
void NativeWidgetMac::SetCursor(gfx::NativeCursor cursor) {
NOTIMPLEMENTED();
if (bridge_)
bridge_->SetCursor(cursor);
}
bool NativeWidgetMac::IsMouseEventsEnabled() const {
......
......@@ -7,6 +7,9 @@
#import <Cocoa/Cocoa.h>
#include "base/run_loop.h"
#import "ui/events/test/cocoa_test_event_utils.h"
#include "ui/events/test/event_generator.h"
#include "ui/views/native_cursor.h"
#include "ui/views/test/test_widget_observer.h"
#include "ui/views/test/widget_test.h"
......@@ -257,5 +260,82 @@ TEST_F(NativeWidgetMacTest, MiniaturizeExternally) {
widget->CloseNow();
}
// Simple view for the SetCursor test that overrides View::GetCursor().
class CursorView : public View {
public:
CursorView(int x, NSCursor* cursor) : cursor_(cursor) {
SetBounds(x, 0, 100, 300);
}
// View:
gfx::NativeCursor GetCursor(const ui::MouseEvent& event) override {
return cursor_;
}
private:
NSCursor* cursor_;
DISALLOW_COPY_AND_ASSIGN(CursorView);
};
// Test for Widget::SetCursor(). There is no Widget::GetCursor(), so this uses
// -[NSCursor currentCursor] to validate expectations. Note that currentCursor
// is just "the top cursor on the application's cursor stack.", which is why it
// is safe to use this in a non-interactive UI test with the EventGenerator.
TEST_F(NativeWidgetMacTest, SetCursor) {
NSCursor* arrow = [NSCursor arrowCursor];
NSCursor* hand = GetNativeHandCursor();
NSCursor* ibeam = GetNativeIBeamCursor();
Widget* widget = CreateTopLevelPlatformWidget();
widget->SetBounds(gfx::Rect(0, 0, 300, 300));
widget->GetContentsView()->AddChildView(new CursorView(0, hand));
widget->GetContentsView()->AddChildView(new CursorView(100, ibeam));
widget->Show();
// Events used to simulate tracking rectangle updates. These are not passed to
// toolkit-views, so it only matters whether they are inside or outside the
// content area.
NSEvent* event_in_content = cocoa_test_event_utils::MouseEventAtPoint(
NSMakePoint(100, 100), NSMouseMoved, 0);
NSEvent* event_out_of_content = cocoa_test_event_utils::MouseEventAtPoint(
NSMakePoint(-50, -50), NSMouseMoved, 0);
EXPECT_NE(arrow, hand);
EXPECT_NE(arrow, ibeam);
// At the start of the test, the cursor stack should be empty.
EXPECT_FALSE([NSCursor currentCursor]);
// Use an event generator to ask views code to set the cursor. However, note
// that this does not cause Cocoa to generate tracking rectangle updates.
ui::test::EventGenerator event_generator(GetContext(),
widget->GetNativeWindow());
// Move the mouse over the first view, then simulate a tracking rectangle
// update.
event_generator.MoveMouseTo(gfx::Point(50, 50));
[widget->GetNativeWindow() cursorUpdate:event_in_content];
EXPECT_EQ(hand, [NSCursor currentCursor]);
// A tracking rectangle update not in the content area should forward to
// the native NSWindow implementation, which sets the arrow cursor.
[widget->GetNativeWindow() cursorUpdate:event_out_of_content];
EXPECT_EQ(arrow, [NSCursor currentCursor]);
// Now move to the second view.
event_generator.MoveMouseTo(gfx::Point(150, 50));
[widget->GetNativeWindow() cursorUpdate:event_in_content];
EXPECT_EQ(ibeam, [NSCursor currentCursor]);
// Moving to the third view (but remaining in the content area) should also
// forward to the native NSWindow implementation.
event_generator.MoveMouseTo(gfx::Point(250, 50));
[widget->GetNativeWindow() cursorUpdate:event_in_content];
EXPECT_EQ(arrow, [NSCursor currentCursor]);
widget->CloseNow();
}
} // namespace test
} // namespace views
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment