| // Copyright 2018 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import "content/app_shim_remote_cocoa/web_contents_view_cocoa.h" |
| |
| #include <AppKit/AppKit.h> |
| |
| #include "base/containers/contains.h" |
| #include "base/files/file_util.h" |
| #include "base/files/scoped_temp_dir.h" |
| #import "base/mac/mac_util.h" |
| #include "base/strings/sys_string_conversions.h" |
| #include "base/threading/thread_restrictions.h" |
| #import "content/app_shim_remote_cocoa/web_contents_occlusion_checker_mac.h" |
| #import "content/app_shim_remote_cocoa/web_drag_source_mac.h" |
| #import "content/browser/web_contents/web_contents_view_mac.h" |
| #import "content/browser/web_contents/web_drag_dest_mac.h" |
| #include "content/common/features.h" |
| #include "content/public/browser/content_browser_client.h" |
| #include "content/public/common/content_client.h" |
| #include "ui/base/clipboard/clipboard_constants.h" |
| #include "ui/base/clipboard/clipboard_util_mac.h" |
| #include "ui/base/clipboard/custom_data_helper.h" |
| #include "ui/base/dragdrop/drag_drop_types.h" |
| #include "ui/events/event_utils.h" |
| #include "ui/events/platform_event.h" |
| #include "ui/gfx/image/image.h" |
| #include "ui/resources/grit/ui_resources.h" |
| |
| using content::DropData; |
| using remote_cocoa::mojom::DraggingInfo; |
| using remote_cocoa::mojom::SelectionDirection; |
| |
| namespace remote_cocoa { |
| |
| // DroppedScreenShotCopierMac is a utility to copy screenshots to a usable |
| // directory for PWAs. When screenshots are taken and dragged directly on to an |
| // application, the resulting file can only be opened by the application that it |
| // is passed to. For PWAs, this means that the file dragged to the PWA is not |
| // accessible by the browser, resulting in a failure to open the file. This |
| // class works around that problem by copying such screenshot files. |
| // https://crbug.com/1148078 |
| class DroppedScreenShotCopierMac { |
| public: |
| DroppedScreenShotCopierMac() = default; |
| ~DroppedScreenShotCopierMac() { |
| if (temp_dir_) { |
| base::ScopedAllowBlocking allow_io; |
| temp_dir_.reset(); |
| } |
| } |
| |
| // Examine all entries in `drop_data.filenames`. If any of them look like a |
| // screenshot file, copy the file to a temporary directory. This temporary |
| // directory (and its contents) will be kept alive until `this` is destroyed. |
| void CopyScreenShotsInDropData(content::DropData& drop_data) { |
| for (auto& file_info : drop_data.filenames) { |
| if (IsPathScreenShot(file_info.path)) { |
| base::ScopedAllowBlocking allow_io; |
| if (!temp_dir_) { |
| auto new_temp_dir = std::make_unique<base::ScopedTempDir>(); |
| if (!new_temp_dir->CreateUniqueTempDir()) |
| return; |
| temp_dir_ = std::move(new_temp_dir); |
| } |
| base::FilePath copy_path = |
| temp_dir_->GetPath().Append(file_info.path.BaseName()); |
| if (base::CopyFile(file_info.path, copy_path)) |
| file_info.path = copy_path; |
| } |
| } |
| } |
| |
| private: |
| bool IsPathScreenShot(const base::FilePath& path) const { |
| const std::string& value = path.value(); |
| if (!base::Contains(value, "/var")) { |
| return false; |
| } |
| if (!base::Contains(value, "screencaptureui")) { |
| return false; |
| } |
| return true; |
| } |
| |
| std::unique_ptr<base::ScopedTempDir> temp_dir_; |
| }; |
| |
| } // namespace remote_cocoa |
| |
| // Ensure that the ui::DragDropTypes::DragOperation enum values stay in sync |
| // with NSDragOperation constants, since the code below uses |
| // NSDragOperationToDragOperation to filter invalid values. |
| #define STATIC_ASSERT_ENUM(a, b) \ |
| static_assert(static_cast<int>(a) == static_cast<int>(b), \ |
| "enum mismatch: " #a) |
| STATIC_ASSERT_ENUM(NSDragOperationNone, ui::DragDropTypes::DRAG_NONE); |
| STATIC_ASSERT_ENUM(NSDragOperationCopy, ui::DragDropTypes::DRAG_COPY); |
| STATIC_ASSERT_ENUM(NSDragOperationLink, ui::DragDropTypes::DRAG_LINK); |
| STATIC_ASSERT_ENUM(NSDragOperationMove, ui::DragDropTypes::DRAG_MOVE); |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // WebContentsViewCocoa |
| |
| @implementation WebContentsViewCocoa { |
| // Instances of this class are owned by both `_host` and AppKit. The `_host` |
| // must call `-setHost:nil` in its destructor. |
| raw_ptr<remote_cocoa::mojom::WebContentsNSViewHost> _host; |
| |
| // The interface exported to views::Views that embed this as a sub-view. |
| raw_ptr<ui::ViewsHostableView> _viewsHostableView; |
| |
| BOOL _mouseDownCanMoveWindow; |
| |
| // Utility to copy screenshots to a usable directory for PWAs. This utility |
| // will maintain a temporary directory for such screenshot files until this |
| // WebContents is destroyed. |
| // https://crbug.com/1148078 |
| std::unique_ptr<remote_cocoa::DroppedScreenShotCopierMac> |
| _droppedScreenShotCopier; |
| |
| // Drag variables. |
| WebDragSource* __strong _dragSource; |
| NSDragOperation _dragOperation; |
| |
| BOOL _willSetWebContentsOccludedAfterDelay; |
| } |
| |
| + (void)initialize { |
| // Create the WebContentsOcclusionCheckerMac shared instance. |
| [WebContentsOcclusionCheckerMac sharedInstance]; |
| } |
| |
| - (instancetype)initWithViewsHostableView:(ui::ViewsHostableView*)v { |
| self = [super initWithFrame:NSZeroRect tracking:YES]; |
| if (self != nil) { |
| _viewsHostableView = v; |
| [self registerDragTypes]; |
| |
| [[NSNotificationCenter defaultCenter] |
| addObserver:self |
| selector:@selector(viewDidBecomeFirstResponder:) |
| name:kViewDidBecomeFirstResponder |
| object:nil]; |
| } |
| return self; |
| } |
| |
| - (void)dealloc { |
| // This probably isn't strictly necessary, but can't hurt. |
| [self unregisterDraggedTypes]; |
| |
| [[NSNotificationCenter defaultCenter] removeObserver:self]; |
| [self cancelDelayedSetWebContentsOccluded]; |
| } |
| |
| - (void)enableDroppedScreenShotCopier { |
| DCHECK(!_droppedScreenShotCopier); |
| _droppedScreenShotCopier = |
| std::make_unique<remote_cocoa::DroppedScreenShotCopierMac>(); |
| } |
| |
| - (void)populateDraggingInfo:(DraggingInfo*)info |
| fromNSDraggingInfo:(id<NSDraggingInfo>)nsInfo { |
| NSPoint windowPoint = [nsInfo draggingLocation]; |
| |
| NSPoint viewPoint = [self convertPoint:windowPoint fromView:nil]; |
| NSRect viewFrame = [self frame]; |
| info->location_in_view = |
| gfx::PointF(viewPoint.x, viewFrame.size.height - viewPoint.y); |
| |
| NSPoint screenPoint = [self.window convertPointToScreen:windowPoint]; |
| NSRect screenFrame = self.window.screen.frame; |
| info->location_in_screen = |
| gfx::PointF(screenPoint.x, screenFrame.size.height - screenPoint.y); |
| |
| NSPasteboard* pboard = [nsInfo draggingPasteboard]; |
| NSArray<URLAndTitle*>* urls_and_titles = |
| ui::clipboard_util::URLsAndTitlesFromPasteboard(pboard, |
| /*include_files=*/true); |
| |
| if (urls_and_titles.count) { |
| info->url = GURL(base::SysNSStringToUTF8(urls_and_titles.firstObject.URL)); |
| } |
| info->operation_mask = ui::DragDropTypes::NSDragOperationToDragOperation( |
| [nsInfo draggingSourceOperationMask]); |
| } |
| |
| - (BOOL)allowsVibrancy { |
| // Returning YES will allow rendering this view with vibrancy effect if it is |
| // incorporated into a view hierarchy that uses vibrancy, it will have no |
| // effect otherwise. |
| // For details see Apple documentation on NSView and NSVisualEffectView. |
| return YES; |
| } |
| |
| // Registers for the view for the appropriate drag types. |
| - (void)registerDragTypes { |
| [self registerForDraggedTypes:@[ |
| NSPasteboardTypeFileURL, NSPasteboardTypeHTML, NSPasteboardTypeRTF, |
| NSPasteboardTypeString, NSPasteboardTypeURL, |
| ui::kUTTypeChromiumInitiatedDrag, ui::kUTTypeChromiumDataTransferCustomData, |
| ui::kUTTypeWebKitWebUrlsWithTitles |
| ]]; |
| } |
| |
| - (void)mouseEvent:(NSEvent*)theEvent { |
| if (!_host) |
| return; |
| _host->OnMouseEvent(ui::EventFromNative(base::apple::OwnedNSEvent(theEvent))); |
| } |
| |
| - (void)setMouseDownCanMoveWindow:(BOOL)canMove { |
| _mouseDownCanMoveWindow = canMove; |
| } |
| |
| - (BOOL)mouseDownCanMoveWindow { |
| // This is needed to prevent mouseDowns from moving the window |
| // around. The default implementation returns YES only for opaque |
| // views. WebContentsViewCocoa does not draw itself in any way, but |
| // its subviews do paint their entire frames. Returning NO here |
| // saves us the effort of overriding this method in every possible |
| // subview. |
| return _mouseDownCanMoveWindow; |
| } |
| |
| - (void)startDragWithDropData:(const DropData&)dropData |
| sourceOrigin:(const url::Origin&)sourceOrigin |
| dragOperationMask:(NSDragOperation)operationMask |
| image:(NSImage*)image |
| offset:(NSPoint)offset |
| isPrivileged:(BOOL)isPrivileged { |
| if (!_host) |
| return; |
| |
| NSPoint mouseLocation = [self.window mouseLocationOutsideOfEventStream]; |
| NSEvent* dragEvent = [NSEvent mouseEventWithType:NSEventTypeLeftMouseDragged |
| location:mouseLocation |
| modifierFlags:0 |
| timestamp:NSApp.currentEvent.timestamp |
| windowNumber:self.window.windowNumber |
| context:nil |
| eventNumber:0 |
| clickCount:1 |
| pressure:1.0]; |
| |
| _dragSource = [[WebDragSource alloc] initWithHost:_host |
| dropData:dropData |
| sourceOrigin:sourceOrigin |
| isPrivileged:isPrivileged]; |
| NSDraggingItem* draggingItem = |
| [[NSDraggingItem alloc] initWithPasteboardWriter:_dragSource]; |
| |
| if (!image) { |
| image = content::GetContentClient() |
| ->GetNativeImageNamed(IDR_DEFAULT_FAVICON) |
| .ToNSImage(); |
| } |
| |
| // The frame given to -[NSDraggingItem setDraggingFrame:contents:] will be |
| // interpreted as being in the coordinate system of this view, so convert it |
| // from the coordinate system of the window. |
| mouseLocation = [self convertPoint:mouseLocation fromView:nil]; |
| NSRect imageRect = NSMakeRect(mouseLocation.x, mouseLocation.y, |
| image.size.width, image.size.height); |
| imageRect.origin.x -= offset.x; |
| // Deal with Cocoa's flipped coordinate system. |
| imageRect.origin.y -= image.size.height - offset.y; |
| [draggingItem setDraggingFrame:imageRect contents:image]; |
| |
| _dragOperation = operationMask; |
| |
| // Run the drag operation. |
| [self beginDraggingSessionWithItems:@[ draggingItem ] |
| event:dragEvent |
| source:self]; |
| } |
| |
| // NSDraggingSource methods |
| |
| - (NSDragOperation)draggingSession:(NSDraggingSession*)session |
| sourceOperationMaskForDraggingContext:(NSDraggingContext)context { |
| return _dragOperation; |
| } |
| |
| // Called when a drag initiated in our view ends. |
| - (void)draggingSession:(NSDraggingSession*)session |
| endedAtPoint:(NSPoint)screenPoint |
| operation:(NSDragOperation)operation { |
| if (!_host) { |
| return; |
| } |
| |
| NSPoint localPoint = NSZeroPoint; |
| if (self.window) { |
| NSPoint basePoint = [self.window convertPointFromScreen:screenPoint]; |
| localPoint = [self convertPoint:basePoint fromView:nil]; |
| } |
| |
| // Flip the two points as per Cocoa's coordinate system. |
| NSRect viewFrame = self.frame; |
| NSRect screenFrame = self.window.screen.frame; |
| _host->EndDrag( |
| operation, |
| gfx::PointF(localPoint.x, viewFrame.size.height - localPoint.y), |
| gfx::PointF(screenPoint.x, screenFrame.size.height - screenPoint.y)); |
| |
| // The drag is complete. Disconnect the drag source. |
| [_dragSource webContentsIsGone]; |
| _dragSource = nil; |
| } |
| |
| // NSDraggingDestination methods |
| |
| - (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender { |
| if (!_host) |
| return NSDragOperationNone; |
| |
| // Fill out a DropData from pasteboard. |
| DropData dropData = |
| content::PopulateDropDataFromPasteboard(sender.draggingPasteboard); |
| |
| // Work around screen shot drag-drop permission bugs. |
| // https://crbug.com/1148078 |
| if (_droppedScreenShotCopier) |
| _droppedScreenShotCopier->CopyScreenShotsInDropData(dropData); |
| |
| _host->SetDropData(dropData); |
| |
| auto draggingInfo = DraggingInfo::New(); |
| [self populateDraggingInfo:draggingInfo.get() fromNSDraggingInfo:sender]; |
| uint32_t result = 0; |
| _host->DraggingEntered(std::move(draggingInfo), &result); |
| return result; |
| } |
| |
| - (void)draggingExited:(id<NSDraggingInfo>)sender { |
| if (!_host) |
| return; |
| _host->DraggingExited(); |
| } |
| |
| - (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)sender { |
| if (!_host) |
| return NSDragOperationNone; |
| auto draggingInfo = DraggingInfo::New(); |
| [self populateDraggingInfo:draggingInfo.get() fromNSDraggingInfo:sender]; |
| uint32_t result = 0; |
| _host->DraggingUpdated(std::move(draggingInfo), &result); |
| return result; |
| } |
| |
| - (BOOL)performDragOperation:(id<NSDraggingInfo>)sender { |
| if (!_host) |
| return NO; |
| auto draggingInfo = DraggingInfo::New(); |
| [self populateDraggingInfo:draggingInfo.get() fromNSDraggingInfo:sender]; |
| bool result = false; |
| _host->PerformDragOperation(std::move(draggingInfo), &result); |
| return result; |
| } |
| |
| - (void)clearViewsHostableView { |
| _viewsHostableView = nullptr; |
| } |
| |
| - (void)setHost:(remote_cocoa::mojom::WebContentsNSViewHost*)host { |
| if (!host) { |
| [_dragSource webContentsIsGone]; |
| } |
| _host = host; |
| } |
| |
| - (void)viewDidBecomeFirstResponder:(NSNotification*)notification { |
| if (!_host) |
| return; |
| |
| NSView* view = [notification object]; |
| if (![[self subviews] containsObject:view]) |
| return; |
| |
| NSSelectionDirection ns_direction = static_cast<NSSelectionDirection>( |
| [[notification userInfo][kSelectionDirection] unsignedIntegerValue]); |
| |
| SelectionDirection direction; |
| switch (ns_direction) { |
| case NSDirectSelection: |
| direction = SelectionDirection::kDirect; |
| break; |
| case NSSelectingNext: |
| direction = SelectionDirection::kForward; |
| break; |
| case NSSelectingPrevious: |
| direction = SelectionDirection::kReverse; |
| break; |
| default: |
| return; |
| } |
| _host->OnBecameFirstResponder(direction); |
| } |
| |
| - (void)setWebContentsVisibility:(remote_cocoa::mojom::Visibility)visibility { |
| if (!_host) { |
| return; |
| } |
| auto* content_client = content::GetContentClient(); |
| if (!content_client) { |
| return; |
| } |
| auto* browser = content_client->browser(); |
| if (browser && browser->IsShuttingDown()) { |
| return; |
| } |
| _host->OnWindowVisibilityChanged(visibility); |
| } |
| |
| - (void)performDelayedSetWebContentsOccluded { |
| _willSetWebContentsOccludedAfterDelay = NO; |
| [self setWebContentsVisibility:remote_cocoa::mojom::Visibility::kOccluded]; |
| } |
| |
| - (void)cancelDelayedSetWebContentsOccluded { |
| if (!_willSetWebContentsOccludedAfterDelay) |
| return; |
| |
| [NSObject |
| cancelPreviousPerformRequestsWithTarget:self |
| selector:@selector |
| (performDelayedSetWebContentsOccluded) |
| object:nil]; |
| _willSetWebContentsOccludedAfterDelay = NO; |
| } |
| |
| - (BOOL)willSetWebContentsOccludedAfterDelayForTesting { |
| return _willSetWebContentsOccludedAfterDelay; |
| } |
| |
| - (void)updateWebContentsVisibility: |
| (remote_cocoa::mojom::Visibility)visibility { |
| using remote_cocoa::mojom::Visibility; |
| |
| if (!_host) |
| return; |
| |
| // When a web contents is marked something other than occluded, we want |
| // to act on that right away. For the occluded state, the urgency is |
| // lower and the cost of prematurely switching to the occluded state is |
| // potentially significant. For example, if during browser startup our |
| // window is initially obscured but will become main, our visibility |
| // might be set to occluded and then quickly change to visible. Toggling |
| // between these states would cause our web contents to throw away |
| // resources it needs to display its content and then scramble to reacquire |
| // those resources. There's no need to mark a web contents occluded right |
| // away. Instead, we wait a bit and abort setting the web contents to |
| // occluded if our window switches back to visible or hidden in the meantime. |
| if (visibility != Visibility::kOccluded) { |
| [self cancelDelayedSetWebContentsOccluded]; |
| _host->OnWindowVisibilityChanged(visibility); |
| return; |
| } |
| |
| if (_willSetWebContentsOccludedAfterDelay) |
| return; |
| |
| // Coalesce one second's worth of occlusion updates. |
| const NSTimeInterval kOcclusionUpdateDelayInSeconds = 1.0; |
| [self performSelector:@selector(performDelayedSetWebContentsOccluded) |
| withObject:nil |
| afterDelay:kOcclusionUpdateDelayInSeconds]; |
| _willSetWebContentsOccludedAfterDelay = YES; |
| } |
| |
| - (void)updateWebContentsVisibility { |
| using remote_cocoa::mojom::Visibility; |
| if (!_host) |
| return; |
| |
| Visibility visibility = Visibility::kVisible; |
| if ([self isHiddenOrHasHiddenAncestor] || ![self window]) |
| visibility = Visibility::kHidden; |
| else if ([[self window] isOccluded]) |
| visibility = Visibility::kOccluded; |
| |
| [self updateWebContentsVisibility:visibility]; |
| } |
| |
| - (void)resizeSubviewsWithOldSize:(NSSize)oldBoundsSize { |
| // Subviews do not participate in auto layout unless the the size this view |
| // changes. This allows RenderWidgetHostViewMac::SetBounds(..) to select a |
| // size of the subview that differs from its superview in preparation for an |
| // upcoming WebContentsView resize. |
| // See http://crbug.com/264207 and http://crbug.com/655112. |
| } |
| |
| - (void)setFrameSize:(NSSize)newSize { |
| [super setFrameSize:newSize]; |
| |
| // Perform manual layout of subviews, e.g., when the window size changes. |
| for (NSView* subview in [self subviews]) |
| [subview setFrame:[self bounds]]; |
| } |
| |
| - (void)viewWillMoveToWindow:(NSWindow*)newWindow { |
| NSNotificationCenter* notificationCenter = |
| [NSNotificationCenter defaultCenter]; |
| |
| NSWindow* oldWindow = [self window]; |
| |
| if (oldWindow) { |
| [notificationCenter |
| removeObserver:self |
| name:NSWindowDidChangeOcclusionStateNotification |
| object:oldWindow]; |
| } |
| |
| if (newWindow) { |
| [notificationCenter addObserver:self |
| selector:@selector(windowChangedOcclusionState:) |
| name:NSWindowDidChangeOcclusionStateNotification |
| object:newWindow]; |
| } |
| } |
| |
| - (void)windowChangedOcclusionState:(NSNotification*)aNotification { |
| // Only respond to occlusion notifications sent by the occlusion checker. |
| NSDictionary* userInfo = [aNotification userInfo]; |
| NSString* occlusionCheckerKey = [WebContentsOcclusionCheckerMac className]; |
| if (userInfo[occlusionCheckerKey] != nil) |
| [self updateWebContentsVisibility]; |
| } |
| |
| - (void)viewDidMoveToWindow { |
| [self updateWebContentsVisibility]; |
| } |
| |
| - (void)viewDidHide { |
| [self updateWebContentsVisibility]; |
| } |
| |
| - (void)viewDidUnhide { |
| [self updateWebContentsVisibility]; |
| } |
| |
| // ViewsHostable protocol implementation. |
| - (ui::ViewsHostableView*)viewsHostableView { |
| return _viewsHostableView; |
| } |
| |
| @end |
| |
| @implementation NSWindow (WebContentsViewCocoa) |
| |
| // Collects the WebContentsViewCocoas contained in the view hierarchy |
| // rooted at `view` in the array `webContents`. |
| - (void)_addWebContentsViewCocoasFromView:(NSView*)view |
| toArray: |
| (NSMutableArray<WebContentsViewCocoa*>*) |
| webContents |
| haltAfterFirst:(BOOL)haltAfterFirst { |
| for (NSView* subview in [view subviews]) { |
| if ([subview isKindOfClass:[WebContentsViewCocoa class]]) { |
| [webContents addObject:(WebContentsViewCocoa*)subview]; |
| if (haltAfterFirst) { |
| return; |
| } |
| } else { |
| [self _addWebContentsViewCocoasFromView:subview |
| toArray:webContents |
| haltAfterFirst:haltAfterFirst]; |
| } |
| } |
| } |
| |
| - (NSArray<WebContentsViewCocoa*>*)webContentsViewCocoa { |
| NSMutableArray<WebContentsViewCocoa*>* webContents = [NSMutableArray array]; |
| |
| [self _addWebContentsViewCocoasFromView:[self contentView] |
| toArray:webContents |
| haltAfterFirst:NO]; |
| |
| return webContents; |
| } |
| |
| - (BOOL)containsWebContentsViewCocoa { |
| NSMutableArray<WebContentsViewCocoa*>* webContents = [NSMutableArray array]; |
| |
| [self _addWebContentsViewCocoasFromView:[self contentView] |
| toArray:webContents |
| haltAfterFirst:YES]; |
| |
| return webContents.count > 0; |
| } |
| |
| @end |