Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(2013)

Unified Diff: ui/views/cocoa/bridged_native_widget.mm

Issue 1146873002: [MacViews] Enable dragging a window by its caption/draggable areas. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Sync and rebase Created 5 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « ui/views/cocoa/bridged_native_widget.h ('k') | ui/views/cocoa/bridged_native_widget_interactive_uitest.mm » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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 3e281b595559f1fc079268f30458cbfa4e971d9a..ec3f7eb1d582d59110079617c7eb68c8048c2c11 100644
--- a/ui/views/cocoa/bridged_native_widget.mm
+++ b/ui/views/cocoa/bridged_native_widget.mm
@@ -7,10 +7,12 @@
#import <objc/runtime.h>
#include "base/logging.h"
+#import "base/mac/foundation_util.h"
#include "base/mac/mac_util.h"
#import "base/mac/sdk_forward_declarations.h"
#include "base/thread_task_runner_handle.h"
#import "ui/base/cocoa/constrained_window/constrained_window_animation.h"
+#include "ui/base/hit_test.h"
#include "ui/base/ime/input_method.h"
#include "ui/base/ime/input_method_factory.h"
#include "ui/base/ui_base_switches_util.h"
@@ -78,6 +80,104 @@ gfx::Size GetClientSizeForWindowSize(NSWindow* window,
return gfx::Size([window contentRectForFrameRect:frame_rect].size);
}
+BOOL WindowWantsMouseDownReposted(NSEvent* ns_event) {
+ id delegate = [[ns_event window] delegate];
+ return
+ [delegate
+ respondsToSelector:@selector(shouldRepostPendingLeftMouseDown:)] &&
+ [delegate shouldRepostPendingLeftMouseDown:[ns_event locationInWindow]];
+}
+
+// Check if a mouse-down event should drag the window. If so, repost the event.
+NSEvent* RepostEventIfHandledByWindow(NSEvent* ns_event) {
+ enum RepostState {
+ // Nothing reposted: hit-test new mouse-downs to see if they need to be
+ // ignored and reposted after changing draggability.
+ NONE,
+ // Expecting the next event to be the reposted event: let it go through.
+ EXPECTING_REPOST,
+ // If, while reposting, another mousedown was received: when the reposted
+ // event is seen, ignore it.
+ REPOST_CANCELLED,
+ };
+
+ // Which repost we're expecting to receive.
+ static RepostState repost_state = NONE;
+ // The event number of the reposted event. This let's us track whether an
+ // event is actually the repost since user-generated events have increasing
+ // event numbers. This is only valid while |repost_state != NONE|.
+ static NSInteger reposted_event_number;
+
+ NSInteger event_number = [ns_event eventNumber];
+
+ // The logic here is a bit convoluted because we want to mitigate race
+ // conditions if somehow a different mouse-down occurs between reposts.
+ // Specifically, we want to avoid:
+ // - BridgedNativeWidget's draggability getting out of sync (e.g. if it is
+ // draggable outside of a repost cycle),
+ // - any repost loop.
+
+ if (repost_state == NONE) {
+ if (WindowWantsMouseDownReposted(ns_event)) {
+ repost_state = EXPECTING_REPOST;
+ reposted_event_number = event_number;
+ CGEventPost(kCGSessionEventTap, [ns_event CGEvent]);
+ return nil;
+ }
+
+ return ns_event;
+ }
+
+ if (repost_state == EXPECTING_REPOST) {
+ // Call through so that the window is made non-draggable again.
+ WindowWantsMouseDownReposted(ns_event);
+
+ if (reposted_event_number == event_number) {
+ // Reposted event received.
+ repost_state = NONE;
+ return nil;
+ }
+
+ // We were expecting a repost, but since this is a new mouse-down, cancel
+ // reposting and allow event to continue as usual.
+ repost_state = REPOST_CANCELLED;
+ return ns_event;
+ }
+
+ DCHECK_EQ(REPOST_CANCELLED, repost_state);
+ if (reposted_event_number == event_number) {
+ // Reposting was cancelled, now that we've received the event, we don't
+ // expect to see it again.
+ repost_state = NONE;
+ return nil;
+ }
+
+ return ns_event;
+}
+
+// Support window caption/draggable regions.
+// In AppKit, non-client regions are set by overriding
+// -[NSView mouseDownCanMoveWindow]. NSApplication caches this area as views are
+// installed and performs window moving when mouse-downs land in the area.
+// In Views, non-client regions are determined via hit-tests when the event
+// occurs.
+// To bridge the two models, we monitor mouse-downs with
+// +[NSEvent addLocalMonitorForEventsMatchingMask:handler:]. This receives
+// events after window dragging is handled, so for mouse-downs that land on a
+// draggable point, we cancel the event and repost it at the CGSessionEventTap
+// level so that window dragging will be handled again.
+void SetupDragEventMonitor() {
+ static id monitor = nil;
+ if (monitor)
+ return;
+
+ monitor = [NSEvent
+ addLocalMonitorForEventsMatchingMask:NSLeftMouseDownMask
+ handler:^NSEvent*(NSEvent* ns_event) {
+ return RepostEventIfHandledByWindow(ns_event);
+ }];
+}
+
} // namespace
namespace views {
@@ -101,6 +201,7 @@ BridgedNativeWidget::BridgedNativeWidget(NativeWidgetMac* parent)
in_fullscreen_transition_(false),
window_visible_(false),
wants_to_be_visible_(false) {
+ SetupDragEventMonitor();
DCHECK(parent);
window_delegate_.reset(
[[ViewsNSWindowDelegate alloc] initWithBridgedNativeWidget:this]);
@@ -548,6 +649,44 @@ void BridgedNativeWidget::OnWindowKeyStatusChangedTo(bool is_key) {
}
}
+bool BridgedNativeWidget::ShouldRepostPendingLeftMouseDown(
+ NSPoint location_in_window) {
+ if (!bridged_view_)
+ return false;
+
+ if ([bridged_view_ mouseDownCanMoveWindow]) {
+ // This is a re-post, the movement has already started, so we can make the
+ // window non-draggable again.
+ SetDraggable(false);
+ return false;
+ }
+
+ gfx::Point point(location_in_window.x,
+ NSHeight([window_ frame]) - location_in_window.y);
+ bool should_move_window =
+ native_widget_mac()->GetWidget()->GetNonClientComponent(point) ==
+ HTCAPTION;
+
+ // Check that the point is not obscured by non-content NSViews.
+ for (NSView* subview : [[bridged_view_ superview] subviews]) {
+ if (subview == bridged_view_.get())
+ continue;
+
+ if (![subview mouseDownCanMoveWindow] &&
+ NSPointInRect(location_in_window, [subview frame])) {
+ should_move_window = false;
+ break;
+ }
+ }
+
+ if (!should_move_window)
+ return false;
+
+ // Make the window draggable, then return true to repost the event.
+ SetDraggable(true);
+ return true;
+}
+
void BridgedNativeWidget::OnSizeConstraintsChanged() {
// Don't modify the size constraints or fullscreen collection behavior while
// in fullscreen or during a transition. OnFullscreenTransitionComplete will
@@ -886,4 +1025,14 @@ NSMutableDictionary* BridgedNativeWidget::GetWindowProperties() const {
return properties;
}
+void BridgedNativeWidget::SetDraggable(bool draggable) {
+ [bridged_view_ setMouseDownCanMoveWindow:draggable];
+ // AppKit will not update its cache of mouseDownCanMoveWindow unless something
+ // changes. Previously we tried adding an NSView and removing it, but for some
+ // reason it required reposting the mouse-down event, and didn't always work.
+ // Calling the below seems to be an effective solution.
+ [window_ setMovableByWindowBackground:NO];
+ [window_ setMovableByWindowBackground:YES];
+}
+
} // namespace views
« no previous file with comments | « ui/views/cocoa/bridged_native_widget.h ('k') | ui/views/cocoa/bridged_native_widget_interactive_uitest.mm » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698