| // Copyright 2016 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/remote_cocoa/app_shim/window_move_loop.h" |
| |
| #include <map> |
| #include <memory> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/run_loop.h" |
| #include "base/strings/stringprintf.h" |
| #import "components/remote_cocoa/app_shim/native_widget_ns_window_bridge.h" |
| #include "ui/display/screen.h" |
| #import "ui/gfx/mac/coordinate_conversion.h" |
| |
| // When event monitors process the events the full list of monitors is cached, |
| // and if we unregister the event monitor that's at the end of the list while |
| // processing the first monitor's handler -- the callback for the unregistered |
| // monitor will still be called even though it's unregistered. This will result |
| // in dereferencing an invalid pointer. |
| // |
| // WeakCocoaWindowMoveLoop is retained by the event monitor and stores weak |
| // pointer for the CocoaWindowMoveLoop, so there will be no invalid memory |
| // access. |
| @interface WeakCocoaWindowMoveLoop : NSObject { |
| @private |
| base::WeakPtr<remote_cocoa::CocoaWindowMoveLoop> _weak; |
| } |
| @end |
| |
| @implementation WeakCocoaWindowMoveLoop |
| - (instancetype)initWithWeakPtr: |
| (const base::WeakPtr<remote_cocoa::CocoaWindowMoveLoop>&)weak { |
| if ((self = [super init])) { |
| _weak = weak; |
| } |
| return self; |
| } |
| |
| - (base::WeakPtr<remote_cocoa::CocoaWindowMoveLoop>&)weak { |
| return _weak; |
| } |
| @end |
| |
| namespace { |
| |
| // This class addresses a macOS 14 issue where child windows don't follow |
| // the parent during tab dragging. |
| class ChildWindowMover { |
| public: |
| ChildWindowMover(NSWindow* window) : window_(window) { |
| initial_parent_origin_ = gfx::Point(window.frame.origin); |
| for (NSWindow* child in window.childWindows) { |
| initial_origins_.emplace_back(child, child.frame.origin); |
| } |
| } |
| |
| // Moves child windows based on a parent origin offset relative to their |
| // initial origins captured at the construction of this class. |
| void MoveByOriginOffset() { |
| if (!window_) { |
| return; |
| } |
| |
| gfx::Point parent_origin = gfx::Point(window_.frame.origin); |
| gfx::Vector2d origin_offset(parent_origin.x() - initial_parent_origin_.x(), |
| parent_origin.y() - initial_parent_origin_.y()); |
| |
| for (const auto& [child, initial_origin] : initial_origins_) { |
| if (!child || child.parentWindow != window_) { |
| continue; |
| } |
| |
| gfx::Point expected_origin = initial_origin + origin_offset; |
| // On macOS 14, child windows occasionally fail to follow their parent |
| // during tab dragging. A workaround for this issue is to temporarily |
| // remove the child window, set its frame origin, and then re-add it. |
| [window_ removeChildWindow:child]; |
| [child |
| setFrameOrigin:NSMakePoint(expected_origin.x(), expected_origin.y())]; |
| [window_ addChildWindow:child ordered:NSWindowAbove]; |
| } |
| } |
| |
| private: |
| NSWindow* __weak window_; |
| std::vector<std::pair<NSWindow * __weak, gfx::Point>> initial_origins_; |
| gfx::Point initial_parent_origin_; |
| }; |
| |
| } // namespace |
| |
| namespace remote_cocoa { |
| |
| CocoaWindowMoveLoop::CocoaWindowMoveLoop(NativeWidgetNSWindowBridge* owner, |
| const NSPoint& initial_mouse_in_screen) |
| : owner_(owner), |
| initial_mouse_in_screen_(initial_mouse_in_screen), |
| weak_factory_(this) {} |
| |
| CocoaWindowMoveLoop::~CocoaWindowMoveLoop() { |
| // Handle the pathological case, where |this| is destroyed while running. |
| if (exit_reason_ref_) { |
| *exit_reason_ref_ = WINDOW_DESTROYED; |
| std::move(quit_closure_).Run(); |
| } |
| |
| owner_ = nullptr; |
| } |
| |
| bool CocoaWindowMoveLoop::Run() { |
| LoopExitReason exit_reason = ENDED_EXTERNALLY; |
| exit_reason_ref_ = &exit_reason; |
| NSWindow* window = owner_->ns_window(); |
| const NSRect initial_frame = [window frame]; |
| __block ChildWindowMover child_window_mover(window); |
| |
| base::RunLoop run_loop; |
| quit_closure_ = run_loop.QuitClosure(); |
| |
| // Will be retained by the monitor handler block. |
| WeakCocoaWindowMoveLoop* weak_cocoa_window_move_loop = |
| [[WeakCocoaWindowMoveLoop alloc] |
| initWithWeakPtr:weak_factory_.GetWeakPtr()]; |
| |
| __block BOOL has_moved = NO; |
| screen_disabler_ = std::make_unique<gfx::ScopedCocoaDisableScreenUpdates>(); |
| |
| // Esc keypress is handled by EscapeTracker, which is installed by |
| // TabDragController. |
| NSEventMask mask = NSEventMaskLeftMouseUp | NSEventMaskLeftMouseDragged | |
| NSEventMaskMouseMoved; |
| auto handler = ^NSEvent*(NSEvent* event) { |
| // The docs say this always runs on the main thread, but if it didn't, |
| // it would explain https://crbug.com/876493, so let's make sure. |
| CHECK(NSThread.isMainThread); |
| |
| CocoaWindowMoveLoop* strong = [weak_cocoa_window_move_loop weak].get(); |
| if (!strong || !strong->exit_reason_ref_) { |
| // By this point CocoaWindowMoveLoop was deleted while processing this |
| // same event, and this event monitor was not unregistered in time. See |
| // the WeakCocoaWindowMoveLoop comment above. |
| // Continue processing the event. |
| return event; |
| } |
| |
| if ([event type] == NSEventTypeLeftMouseDragged) { |
| const NSPoint mouse_in_screen = [NSEvent mouseLocation]; |
| gfx::Vector2d mouse_offset( |
| mouse_in_screen.x - initial_mouse_in_screen_.x, |
| mouse_in_screen.y - initial_mouse_in_screen_.y); |
| NSRect ns_frame = |
| NSOffsetRect(initial_frame, mouse_offset.x(), mouse_offset.y()); |
| [window setFrame:ns_frame display:NO animate:NO]; |
| child_window_mover.MoveByOriginOffset(); |
| // `setFrame:...` may have destroyed `this`, so do the weak check again. |
| bool is_valid = [weak_cocoa_window_move_loop weak].get() == strong; |
| if (is_valid && !has_moved) { |
| has_moved = YES; |
| strong->screen_disabler_.reset(); |
| } |
| |
| return event; |
| } |
| |
| // In theory, we shouldn't see any kind of NSEventTypeMouseMoved, but if we |
| // see one and the left button isn't pressed, we know for a fact that we |
| // missed a NSEventTypeLeftMouseUp. |
| BOOL unexpectedMove = [event type] == NSEventTypeMouseMoved && |
| ([NSEvent pressedMouseButtons] & 1) != 1; |
| if (unexpectedMove || [event type] == NSEventTypeLeftMouseUp) { |
| *strong->exit_reason_ref_ = MOUSE_UP; |
| std::move(strong->quit_closure_).Run(); |
| } |
| return event; // Process the MouseUp. |
| }; |
| id monitor = [NSEvent addLocalMonitorForEventsMatchingMask:mask |
| handler:handler]; |
| |
| run_loop.Run(); |
| [NSEvent removeMonitor:monitor]; |
| |
| if (exit_reason != WINDOW_DESTROYED && exit_reason != ENDED_EXTERNALLY) { |
| exit_reason_ref_ = nullptr; // Ensure End() doesn't replace the reason. |
| owner_->EndMoveLoop(); // Deletes |this|. |
| } |
| |
| return exit_reason == MOUSE_UP; |
| } |
| |
| void CocoaWindowMoveLoop::End() { |
| screen_disabler_.reset(); |
| if (exit_reason_ref_) { |
| DCHECK_EQ(*exit_reason_ref_, ENDED_EXTERNALLY); |
| // Ensure the destructor doesn't replace the reason. |
| exit_reason_ref_ = nullptr; |
| std::move(quit_closure_).Run(); |
| } |
| } |
| |
| } // namespace remote_cocoa |