// 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