1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203
|
// 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
|