Index: ui/views/cocoa/bridged_native_widget.mm |
diff --git a/ui/views/cocoa/bridged_native_widget.mm b/ui/views/cocoa/bridged_native_widget.mm |
index 6f5dec0f96864ede56c8a7cf940de5ebf964aa92..3671b1eb63511e47b7aeda036ad7a56dcf13cc5a 100644 |
--- a/ui/views/cocoa/bridged_native_widget.mm |
+++ b/ui/views/cocoa/bridged_native_widget.mm |
@@ -12,6 +12,7 @@ |
#import "base/mac/foundation_util.h" |
#include "base/mac/mac_util.h" |
#import "base/mac/sdk_forward_declarations.h" |
+#include "base/run_loop.h" |
#include "base/thread_task_runner_handle.h" |
#include "ui/accelerated_widget_mac/window_resize_helper_mac.h" |
#import "ui/base/cocoa/constrained_window/constrained_window_animation.h" |
@@ -35,16 +36,6 @@ |
#include "ui/views/widget/widget_aura_utils.h" |
#include "ui/views/widget/widget_delegate.h" |
-extern "C" { |
- |
-typedef int32_t CGSConnection; |
-CGSConnection _CGSDefaultConnection(); |
-CGError CGSSetWindowBackgroundBlurRadius(CGSConnection connection, |
- NSInteger windowNumber, |
- int radius); |
- |
-} |
- |
// The NSView that hosts the composited CALayer drawing the UI. It fills the |
// window but is not hittable so that accessibility hit tests always go to the |
// BridgedContentView. |
@@ -94,6 +85,8 @@ const int kResizeAreaCornerSize = 12; |
int kWindowPropertiesKey; |
+bool g_ignore_next_mouse_down_for_draggable_regions = false; |
+ |
float GetDeviceScaleFactorFromView(NSView* view) { |
gfx::Display display = |
gfx::Screen::GetScreen()->GetDisplayNearestWindow(view); |
@@ -191,6 +184,11 @@ NSEvent* RepostEventIfHandledByWindow(NSEvent* ns_event) { |
// - any repost loop. |
if (repost_state == NONE) { |
+ if (g_ignore_next_mouse_down_for_draggable_regions) { |
+ g_ignore_next_mouse_down_for_draggable_regions = false; |
+ return ns_event; |
+ } |
+ |
if (WindowWantsMouseDownReposted(ns_event)) { |
repost_state = EXPECTING_REPOST; |
reposted_event_number = event_number; |
@@ -263,10 +261,215 @@ scoped_refptr<base::SingleThreadTaskRunner> GetCompositorTaskRunner() { |
return task_runner ? task_runner : base::ThreadTaskRunnerHandle::Get(); |
} |
+CGPoint GetCGMousePosition() { |
+ return CGEventGetLocation( |
+ base::ScopedCFTypeRef<CGEventRef>(CGEventCreate(nullptr))); |
+} |
+ |
+void SendCustomLeftMouseEvent(CGEventType type) { |
+ base::ScopedCFTypeRef<CGEventRef> event( |
+ CGEventCreateMouseEvent( |
+ nullptr, type, GetCGMousePosition(), |
+ kCGMouseButtonLeft)); |
+ CGEventSetIntegerValueField(event, kCGEventSourceUserData, 1); |
+ CGEventPost(kCGSessionEventTap, event); |
+} |
+ |
} // namespace |
+extern "C" { |
+ |
+typedef int32_t CGSConnection; |
+CGSConnection _CGSDefaultConnection(); |
+CGError CGSSetWindowBackgroundBlurRadius(CGSConnection connection, |
+ NSInteger windowNumber, |
+ int radius); |
+ |
+} |
+ |
+// CocoaWindowMoveLoop can be deleted just before its local event monitor is |
+// processed and in that case it's too late to call removeMonitor: and we have a |
+// dangling this pointer. Use a proxy Obj-C class to store the weakptr. |
+@interface WeakCocoaWindowMoveLoop : NSObject { |
+ @private |
+ base::WeakPtr<views::CocoaWindowMoveLoop> weak_; |
+} |
+@end |
+ |
+@implementation WeakCocoaWindowMoveLoop |
+- (id)initWithWeakPtr:(const base::WeakPtr<views::CocoaWindowMoveLoop>&)weak { |
+ if (self = [super init]) { |
+ weak_ = weak; |
+ } |
+ return self; |
+} |
+ |
+- (base::WeakPtr<views::CocoaWindowMoveLoop>&)weak { |
+ return weak_; |
+} |
+@end |
+ |
namespace views { |
+class CocoaWindowMoveLoop { |
tapted
2016/03/10 11:51:19
Move this to its own file (with WeakCocoaMoveLoop?
themblsha
2016/03/10 17:18:58
Oh, right, you told me about that and it seems tha
|
+ public: |
+ explicit CocoaWindowMoveLoop(BridgedNativeWidget* owner) |
+ : owner_(owner), |
+ run_loop_(new base::RunLoop), |
+ quit_closure_(run_loop_->QuitClosure()), |
+ weak_factory_(this) { |
+ // AppKit continues to send mouse events to the window, but toolkit-views |
+ // goes berserk if it gets them during RunMoveLoop(). |
tapted
2016/03/10 11:51:19
hehe - I remember writing `berserk`... Perhaps "bu
themblsha
2016/03/10 17:18:58
Done.
|
+ [owner_->ns_view() setIgnoreMouseEvents:YES]; |
+ owner_->SetDraggable(true); |
+ } |
+ |
+ ~CocoaWindowMoveLoop() { |
+ owner_->SetDraggable(false); |
+ // Note the crafted events in Run() should not make their way through to |
+ // toolkit-views, but regular events after that should be. So stop ignoring. |
+ [owner_->ns_view() setIgnoreMouseEvents:NO]; |
+ |
+ // Handle the pathological case, where |this| is destroyed while running. |
+ if (exit_reason_ref_) { |
+ *exit_reason_ref_ = WINDOW_DESTROYED; |
+ quit_closure_.Run(); |
+ } |
+ |
+ // Handle Run() never being called. |
+ if (monitor_) { |
+ [NSEvent removeMonitor:monitor_]; |
+ monitor_ = nil; |
+ } |
+ owner_ = nullptr; |
+ } |
+ |
+ Widget::MoveLoopResult Run() { |
+ LoopExitReason exit_reason = ENDED_EXTERNALLY; |
+ exit_reason_ref_ = &exit_reason; |
+ |
+ // Move the RunLoop to the stack so our destructor can be called while it is |
+ // running. |
+ scoped_ptr<base::RunLoop> run_loop(std::move(run_loop_)); |
+ |
+ // A new window may have just been created, so post an event at the session |
+ // tap level to initiate a window drag. |
+ // TODO(tapted): Move this "inside" the window or stuff breaks when dragging |
+ // the last tab/s off a browser. |
+ SendCustomLeftMouseEvent(kCGEventLeftMouseDown); |
+ |
+ WeakCocoaWindowMoveLoop* proxy_quit_closure = |
+ [[[WeakCocoaWindowMoveLoop alloc] |
+ initWithWeakPtr:weak_factory_.GetWeakPtr()] autorelease]; |
+ |
+ NSEventMask mask = |
+ NSLeftMouseUpMask | NSKeyDownMask | NSLeftMouseDraggedMask; |
+ monitor_ = [NSEvent addLocalMonitorForEventsMatchingMask:mask |
+ handler:^NSEvent*(NSEvent* event) { |
+ if ([event type] == NSLeftMouseDragged) { |
+ // AppKit doesn't supply position updates during a drag, so post a |
+ // task to notify observers once AppKit has moved the window. |
+ base::MessageLoop::current()->PostTask( |
+ FROM_HERE, base::Bind(&BridgedNativeWidget::OnPositionChanged, |
+ base::Unretained(owner_))); |
+ return event; |
+ } |
+ |
+ CocoaWindowMoveLoop* thiss = [proxy_quit_closure weak].get(); |
+ if (!thiss) |
+ return nil; |
+ |
+ thiss->quit_closure_.Run(); |
+ if ([event type] == NSLeftMouseUp) { |
+ *thiss->exit_reason_ref_ = MOUSE_UP; |
+ return event; // Process the MouseUp. |
+ } |
+ *thiss->exit_reason_ref_ = ESCAPE_PRESSED; |
+ return nil; // Swallow the keypress. |
+ }]; |
+ |
+ // NSKeyDownMask doesn't work inside addLocalMonitorForEventsMatchingMask: |
+ // the event is swallowed by the window move loop before it gets to -[NSApp |
+ // sendEvent:]. To see an escape keypress, hook in an event tap lower. |
+ |
+ run_loop->Run(); |
+ |
+ if (exit_reason != WINDOW_DESTROYED && exit_reason != ENDED_EXTERNALLY) { |
+ exit_reason_ref_ = nullptr; // Ensure End() doesn't replace the reason. |
+ owner_->EndMoveLoop(); // Deletes |this|. |
+ } |
+ |
+ if (exit_reason != MOUSE_UP) { |
+ // Tell AppKit to stop moving the window. Otherwise, AppKit will refuse to |
+ // start a new window drag. Note the window being dragged is going away in |
+ // this case, so it doesn't really matter where the event is located. |
+ SendCustomLeftMouseEvent(kCGEventLeftMouseUp); |
+ |
+ if (exit_reason == ENDED_EXTERNALLY) { |
+ // When not canceled, the non-moving drag in the original window must |
+ // resume. To do this, AppKit needs to see a mouseDown so that it sends |
+ // the correct events. Ideally, it also needs to be at the original |
+ // offset, so that it hits a non-draggable region of the original |
+ // window: The tab being dragged may move some distance from the cursor |
+ // when it "snaps in", so the cursor may not be over a tab. Sadly, this |
+ // method doesn't know which window that is. But all that really needs |
+ // to be done is to prevent a custom-dragging area from starting a |
+ // window-drag. So hook into the logic in RepostEventIfHandledByWindow() |
+ // by setting a flag here. Note this assumes the custom mouseDown event |
+ // is guaranteed to hit another BridgedNativeWidget when it gets to the |
+ // front of the event queue. |
+ // TODO(tapted): A better fix would be to keep the temporary window |
+ // around and never call EndMoveLoop() on Mac, making this block of code |
+ // obsolete. |
+ g_ignore_next_mouse_down_for_draggable_regions = true; |
+ SendCustomLeftMouseEvent(kCGEventLeftMouseDown); |
+ } |
+ } |
+ |
+ return exit_reason == ESCAPE_PRESSED |
+ ? Widget::MOVE_LOOP_CANCELED : Widget::MOVE_LOOP_SUCCESSFUL; |
+ } |
+ |
+ void End() { |
+ if (exit_reason_ref_) { |
+ DCHECK_EQ(*exit_reason_ref_, ENDED_EXTERNALLY); |
+ // Ensure the destructor doesn't replace the reason. |
+ exit_reason_ref_ = nullptr; |
+ quit_closure_.Run(); |
+ } |
+ } |
+ |
+ static CGEventRef EscapeKeypressMonitor(CGEventTapProxy proxy, |
+ CGEventType type, |
+ CGEventRef event, |
+ void* refcon) { |
+ CocoaWindowMoveLoop* self = static_cast<CocoaWindowMoveLoop*>(refcon); |
+ (void)self; |
+ return event; |
+ } |
+ |
+ private: |
+ enum LoopExitReason { |
+ ENDED_EXTERNALLY, |
+ ESCAPE_PRESSED, |
+ MOUSE_UP, |
+ WINDOW_DESTROYED, |
+ }; |
+ |
+ BridgedNativeWidget* owner_; // Weak. Owns this. |
+ scoped_ptr<base::RunLoop> run_loop_; |
+ id monitor_ = nil; |
+ |
+ // Pointer to a stack variable holding the exit reason. |
+ LoopExitReason* exit_reason_ref_ = nullptr; |
+ base::Closure quit_closure_; |
+ |
+ // WeakPtrFactory for event monitor safety. |
+ base::WeakPtrFactory<CocoaWindowMoveLoop> weak_factory_; |
+ |
+ DISALLOW_COPY_AND_ASSIGN(CocoaWindowMoveLoop); |
+}; |
+ |
// static |
gfx::Size BridgedNativeWidget::GetWindowSizeForClientSize( |
NSWindow* window, |
@@ -569,6 +772,49 @@ bool BridgedNativeWidget::HasCapture() { |
return mouse_capture_ && mouse_capture_->IsActive(); |
} |
+Widget::MoveLoopResult BridgedNativeWidget::RunMoveLoop( |
+ const gfx::Vector2d& drag_offset) { |
+ DCHECK(!HasCapture()); |
+ DCHECK(!window_move_loop_); |
+ |
+ // First, position the window in the right place. The point |drag_offset| |
+ // away from the top-left corner needs to be positioned under the mouse. |
+ // TODO(tapted): Figure out why the toolkit-views drag controller doesn't get |
+ // this right when it first initializes the Widget. |
+ NSPoint mouse_in_screen = gfx::ScreenPointToNSPoint( |
+ gfx::Screen::GetScreen()->GetCursorScreenPoint()); |
+ NSRect frame = [window_ frame]; |
+ frame.origin.x = mouse_in_screen.x - drag_offset.x(); |
+ frame.origin.y = mouse_in_screen.y - NSHeight(frame) + drag_offset.y(); |
+ |
+ // After setting the frame to correct the initial offset, the drag controller |
+ // may immediately want to quit when it's notified of the new bounds. So the |
+ // MoveLoop must be set up before the call to setFrame. |
+ window_move_loop_.reset(new CocoaWindowMoveLoop(this)); |
+ |
+ // Animating may provide a less janky UX, but something custom would be |
+ // required so that it follows updates to the mouse position. |
+ [window_ setFrame:frame display:YES animate:NO]; |
+ |
+ // Setting the frame will call OnWidgetBoundsChanged(), which could result in |
+ // a call to EndMoveLoop(). |
+ if (!window_move_loop_) |
+ return Widget::MOVE_LOOP_SUCCESSFUL; |
+ |
+ return window_move_loop_->Run(); |
+ |
+ // |this| may be destroyed during the RunLoop, causing it to exit early. |
+ // Even if that doesn't happen, CocoaWindowMoveLoop will clean itself up by |
+ // calling EndMoveLoop(). So window_move_loop_ will always be null before the |
+ // function returns. But don't DCHECK since |this| might not be valid. |
+} |
+ |
+void BridgedNativeWidget::EndMoveLoop() { |
+ DCHECK(window_move_loop_); |
+ window_move_loop_->End(); |
+ window_move_loop_.reset(); |
+} |
+ |
void BridgedNativeWidget::SetNativeWindowProperty(const char* name, |
void* value) { |
NSString* key = [NSString stringWithUTF8String:name]; |
@@ -590,6 +836,7 @@ void BridgedNativeWidget::SetCursor(NSCursor* cursor) { |
} |
void BridgedNativeWidget::OnWindowWillClose() { |
+ DCHECK(!drag_run_loop_); |
if (parent_) { |
parent_->RemoveChildWindow(this); |
parent_ = nullptr; |
@@ -692,6 +939,10 @@ void BridgedNativeWidget::OnSizeChanged() { |
[bridged_view_ updateWindowMask]; |
} |
+void BridgedNativeWidget::OnPositionChanged() { |
+ native_widget_mac_->GetWidget()->OnNativeWidgetMove(); |
+} |
+ |
void BridgedNativeWidget::OnVisibilityChanged() { |
OnVisibilityChangedTo([window_ isVisible]); |
} |