native/util_mac.mm (508 lines of code) (raw):

// Copyright (c) 2013 The Chromium Embedded Framework Authors. All rights // reserved. Use of this source code is governed by a BSD-style license that // can be found in the LICENSE file. #import "util_mac.h" #import "util.h" #import <Cocoa/Cocoa.h> #import <Foundation/NSLock.h> #import <jni.h> #include <objc/runtime.h> #include "include/base/cef_callback.h" #include "include/cef_app.h" #include "include/cef_application_mac.h" #include "include/cef_browser.h" #include "include/cef_path_util.h" #include "JCEFThreadUtilities.h" #include "client_app.h" #include "client_handler.h" #include "critical_wait.h" #include "jni_util.h" #include "render_handler.h" #include "temp_window.h" #include "window_handler.h" #include <chrono> #include <thread> namespace { static std::set<CefWindowHandle> g_browsers_; static CriticalLock g_browsers_lock_; id g_mouse_monitor_ = nil; static CefRefPtr<ClientApp> g_client_app_ = nullptr; bool g_handling_send_event = false; bool g_before_shutdown = false; bool g_after_shutdown = false; bool isBrowserExists(CefWindowHandle handle) { g_browsers_lock_.Lock(); const bool result = g_browsers_.count(handle) > 0; g_browsers_lock_.Unlock(); return result; } } // namespace // Used for passing data to/from ClientHandler initialize:. @interface InitializeParams : NSObject { @public std::shared_ptr<CefMainArgs> args_; CefSettings settings_; CefRefPtr<ClientApp> application_; bool result_; } @end @implementation InitializeParams @end // Used for passing data to/from ClientHandler setVisiblity:. @interface SetVisibilityParams : NSObject { @public CefWindowHandle handle_; bool isVisible_; } @end @implementation SetVisibilityParams @end @interface ShutdownParams : NSObject { @public TempWindowMac * pointer_; } @end @implementation ShutdownParams @end // Obj-C Wrapper Class to be called by "performSelectorOnMainThread". @interface CefHandler : NSObject { } + (void)initialize:(InitializeParams*)params; + (void)shutdown:(ShutdownParams*)params; + (void)doMessageLoopWork; + (void)setVisibility:(SetVisibilityParams*)params; @end // interface CefHandler @interface NSAutoreleasePool (JCEFAutoreleasePool) - (void)_swizzled_drain; @end @implementation NSAutoreleasePool (JCEFAutoreleasePool) + (void)load { Method originalDrain = class_getInstanceMethod([NSAutoreleasePool class], @selector(drain)); Method swizzledDrain = class_getInstanceMethod(self, @selector(_swizzled_drain)); method_exchangeImplementations(originalDrain, swizzledDrain); } - (void)_swizzled_drain { // do not up-call during a shutdown when on the main thread to avoid crash if (!g_before_shutdown || g_after_shutdown || ![NSThread isMainThread]) { [self _swizzled_drain]; } } @end // Java provides an NSApplicationAWT implementation that we can't access or // override directly. Therefore add the necessary CefAppProtocol // functionality to NSApplication using categories and swizzling. @interface NSApplication (JCEFApplication) <CefAppProtocol> - (BOOL)isHandlingSendEvent; - (void)setHandlingSendEvent:(BOOL)handlingSendEvent; - (void)_swizzled_sendEvent:(NSEvent*)event; - (void)_swizzled_terminate:(id)sender; - (BOOL)_swizzled_NSMenuItem_accessibilityIsAttributeSettable:(NSAccessibilityAttributeName)attribute; @end @implementation NSApplication (JCEFApplication) // This selector is called very early during the application initialization. + (void)load { // Swap NSApplication::sendEvent with _swizzled_sendEvent. Method original = class_getInstanceMethod(self, @selector(sendEvent:)); Method swizzled = class_getInstanceMethod(self, @selector(_swizzled_sendEvent:)); method_exchangeImplementations(original, swizzled); Method originalTerm = class_getInstanceMethod(self, @selector(terminate:)); Method swizzledTerm = class_getInstanceMethod(self, @selector(_swizzled_terminate:)); method_exchangeImplementations(originalTerm, swizzledTerm); if (!class_getInstanceMethod([NSMenuItem class], @selector(accessibilityIsAttributeSettable:))) { Method method_NSMenuItem_accessibilityIsAttributeSettable = class_getInstanceMethod(self, @selector(_swizzled_NSMenuItem_accessibilityIsAttributeSettable:)); class_addMethod( [NSMenuItem class], @selector(accessibilityIsAttributeSettable:), method_getImplementation(method_NSMenuItem_accessibilityIsAttributeSettable), method_getTypeEncoding(method_NSMenuItem_accessibilityIsAttributeSettable)); } } + (void)setMouseMonitor { g_mouse_monitor_ = [NSEvent addLocalMonitorForEventsMatchingMask:(NSEventMaskLeftMouseDown | NSEventMaskLeftMouseUp | NSEventMaskLeftMouseDragged | NSEventMaskRightMouseDown | NSEventMaskRightMouseUp | NSEventMaskRightMouseDragged | NSEventMaskOtherMouseDown | NSEventMaskOtherMouseUp | NSEventMaskOtherMouseDragged | NSEventMaskScrollWheel | NSEventMaskMouseMoved | NSEventMaskMouseEntered | NSEventMaskMouseExited) handler:^(NSEvent* evt) { // Get corresponding CefWindowHandle of // Java-Canvas NSView* browser = nullptr; NSPoint absPos = [evt locationInWindow]; NSWindow* evtWin = [evt window]; g_browsers_lock_.Lock(); std::set<CefWindowHandle> browsers = g_browsers_; g_browsers_lock_.Unlock(); std::set<CefWindowHandle>::iterator it; for (it = browsers.begin(); it != browsers.end(); ++it) { NSView* wh = CAST_CEF_WINDOW_HANDLE_TO_NSVIEW( *it); NSPoint relPos = [wh convertPoint:absPos fromView:nil]; NSRect frame = [wh frame]; // bounds in superview NSRect bounds = NSMakeRect(0, 0, frame.size.width, frame.size.height); if (evtWin == [wh window] && [wh mouse:relPos inRect:bounds]) { browser = wh; break; } } if (!browser) return evt; // Forward mouse event to browsers parent // (JCEF UI) switch ([evt type]) { case NSEventTypeLeftMouseDown: case NSEventTypeOtherMouseDown: case NSEventTypeRightMouseDown: [[browser superview] mouseDown:evt]; return evt; case NSEventTypeLeftMouseUp: case NSEventTypeOtherMouseUp: case NSEventTypeRightMouseUp: [[browser superview] mouseUp:evt]; return evt; case NSEventTypeLeftMouseDragged: case NSEventTypeOtherMouseDragged: case NSEventTypeRightMouseDragged: [[browser superview] mouseDragged:evt]; return evt; case NSEventTypeMouseMoved: [[browser superview] mouseMoved:evt]; return evt; case NSEventTypeMouseEntered: [[browser superview] mouseEntered:evt]; return evt; case NSEventTypeMouseExited: [[browser superview] mouseExited:evt]; return evt; case NSEventTypeScrollWheel: [[browser superview] scrollWheel:evt]; return evt; default: return evt; } }]; } - (BOOL)isHandlingSendEvent { return g_handling_send_event; } - (void)setHandlingSendEvent:(BOOL)handlingSendEvent { g_handling_send_event = handlingSendEvent; } - (BOOL)_swizzled_NSMenuItem_accessibilityIsAttributeSettable:(NSAccessibilityAttributeName)attribute { return NO; } - (void)_swizzled_sendEvent:(NSEvent*)event { CefScopedSendingEvent sendingEventScoper; // Calls NSApplication::sendEvent due to the swizzling. [self _swizzled_sendEvent:event]; } // This method will be called via Cmd+Q. - (void)_swizzled_terminate:(id)sender { bool continueTerminate = true; if (g_client_app_.get()) { // Call CefApp.handleBeforeTerminate() in Java. Will result in a call // to CefShutdownOnMainThread() via CefApp.shutdown(). continueTerminate = !g_client_app_->HandleTerminate(); } if (continueTerminate && !g_after_shutdown) [[CefHandler class] shutdown:nil]; // [tav] let NSApplication::terminate proceed [self _swizzled_terminate:sender]; } @end @implementation CefHandler // |params| will be released by the caller. + (void)initialize:(InitializeParams*)params { g_client_app_ = params->application_; params->result_ = CefInitialize(*params->args_, params->settings_, g_client_app_.get(), nullptr); } + (void)shutdown:(ShutdownParams*)params { // JBR-5822: to debug intermittent crashes on shutdown use constants from environment int workCount = 10; const char* sval = getenv("JCEF_SHUTDOWN_WORK_COUNT"); if (sval != nullptr) { workCount = atoi(sval); if (workCount < 0) workCount = 0; if (workCount > 100) workCount = 100; fprintf(stderr, "\tPreform CefDoMessageLoopWork %d times before shutdown\n", (int)workCount); } int workPause = 0; sval = getenv("JCEF_SHUTDOWN_WORK_PAUSE"); if (sval != nullptr) { workPause = atoi(sval); if (workPause < 0) workPause = 0; if (workPause > 20) workPause = 20; fprintf(stderr, "\tUse workPause=%d\n", (int)workPause); } // Pump CefDoMessageLoopWork a few times before shutting down. for (int i = 0; i < workCount; ++i) { if (workPause > 0) std::this_thread::sleep_for(std::chrono::milliseconds(workPause)); CefDoMessageLoopWork(); } g_before_shutdown = true; CefShutdown(); g_after_shutdown = true; g_client_app_ = nullptr; if (g_mouse_monitor_) [NSEvent removeMonitor:g_mouse_monitor_]; if (params != nil) { if (params->pointer_ != nullptr) delete params->pointer_; [params release]; } } + (void)doMessageLoopWork { CefDoMessageLoopWork(); } + (void)setVisibility:(SetVisibilityParams*)params { if (g_client_app_) { if (!isBrowserExists(params->handle_)) return; NSView* wh = CAST_CEF_WINDOW_HANDLE_TO_NSVIEW(params->handle_); bool isHidden = [wh isHidden]; if (isHidden == params->isVisible_) { [wh setHidden:!params->isVisible_]; [wh needsDisplay]; [[wh superview] display]; } } [params release]; } @end // implementation CefHandler // Instead of adding CefBrowser as child of the windows main view, a content // view (CefBrowserContentView) is set between the main view and the // CefBrowser. Why? // // The CefBrowserContentView defines the viewable part of the browser view. // In most cases the content view has the same size as the browser view, // but for example if you add CefBrowser to a JScrollPane, you only want to // see the portion of the browser window you're scrolled to. In this case // the sizes of the content view and the browser view differ. // // With other words the CefBrowserContentView clips the CefBrowser to its // displayable content. // // +- - - - - - - - - - - - - - - - - - - - -+ // |/ / CefBrowser/ / / / / / /| // +-------------------------+ / / / <--- invisible part of CefBrowser // | | CefBrowserContentView | / / / | // /| |/ / / // |/ | | / / /| // | <------------------ visible part of CefBrowser // | | | / / / | // /| |/ / / // |/ | | / / /| // | | / / / // | +-------------------------+ / / / | // / / / / / / / / / / // |/ / / / / / / / / / /| // / / / / / / / / / / // | / / / / / / / / / / | // +- - - - - - - - - - - - - - - - - - - - -+ @interface CefBrowserContentView : NSView { CefRefPtr<CefBrowser> cefBrowser; } @property(readonly) BOOL isLiveResizing; - (void)addCefBrowser:(CefRefPtr<CefBrowser>)browser; - (void)destroyCefBrowser; - (void)updateView:(NSDictionary*)dict; @end // interface CefBrowserContentView @implementation CefBrowserContentView @synthesize isLiveResizing; - (id)initWithFrame:(NSRect)frameRect { self = [super initWithFrame:frameRect]; cefBrowser = nullptr; return self; } - (void)dealloc { if (cefBrowser) { util::DestroyCefBrowser(cefBrowser); } [[NSNotificationCenter defaultCenter] removeObserver:self]; cefBrowser = nullptr; [super dealloc]; } - (void)setFrame:(NSRect)frameRect { // Instead of using the passed frame, get the visible rect from java because // the passed frame rect doesn't contain the clipped view part. Otherwise // we'll overlay some parts of the Java UI. if (cefBrowser.get()) { CefRefPtr<ClientHandler> clientHandler = (ClientHandler*)(cefBrowser->GetHost()->GetClient().get()); CefRefPtr<WindowHandler> windowHandler = clientHandler->GetWindowHandler(); if (windowHandler.get() != nullptr) { CefRect rect; windowHandler->GetRect(cefBrowser, rect); util_mac::TranslateRect(self, rect); frameRect = (NSRect){{rect.x, rect.y}, {rect.width, rect.height}}; } } [super setFrame:frameRect]; } - (void)addCefBrowser:(CefRefPtr<CefBrowser>)browser { cefBrowser = browser; // Register for the start and end events of "liveResize" to avoid // Java paint updates while someone is resizing the main window (e.g. by // pulling with the mouse cursor) [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(windowWillStartLiveResize:) name:NSWindowWillStartLiveResizeNotification object:[self window]]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(windowDidEndLiveResize:) name:NSWindowDidEndLiveResizeNotification object:[self window]]; } - (void)destroyCefBrowser { [[NSNotificationCenter defaultCenter] removeObserver:self]; cefBrowser = nullptr; [self removeFromSuperview]; // Also remove all subviews so the CEF objects are released. for (NSView* view in [self subviews]) { [view removeFromSuperview]; } } - (void)windowWillStartLiveResize:(NSNotification*)notification { isLiveResizing = YES; } - (void)windowDidEndLiveResize:(NSNotification*)notification { isLiveResizing = NO; [self setFrame:[self frame]]; } - (void)updateView:(NSDictionary*)dict { NSRect contentRect = NSRectFromString([dict valueForKey:@"content"]); NSRect browserRect = NSRectFromString([dict valueForKey:@"browser"]); NSArray* childs = [self subviews]; for (NSView* child in childs) { [child setFrame:browserRect]; [child setNeedsDisplay:YES]; } [super setFrame:contentRect]; [self setNeedsDisplay:YES]; } @end // implementation CefBrowserContentView namespace util_mac { std::string GetAbsPath(const std::string& path) { char full_path[PATH_MAX]; if (realpath(path.c_str(), full_path) == nullptr) return std::string(); return full_path; } bool IsNSView(void* ptr) { id obj = (id)ptr; bool result = [obj isKindOfClass:[NSView class]]; if (!result) NSLog(@"Expected NSView, found %@", NSStringFromClass([obj class])); return result; } void* GetNSView(void* nsWindow) { if (![(id)nsWindow isKindOfClass:[NSWindow class]]) { NSLog(@"Expected NSWindow, found %@", NSStringFromClass([(id)nsWindow class])); return nullptr; } return [(NSWindow*)nsWindow contentView]; } void retainObj(jlong pointer) { if (pointer != 0) [(NSObject*)pointer retain]; } void releaseObj(jlong pointer) { if (pointer != 0) [(NSObject*)pointer release]; } CefWindowHandle CreateBrowserContentView(NSWindow* window, CefRect& orig) { NSView* mainView = CAST_CEF_WINDOW_HANDLE_TO_NSVIEW([window contentView]); TranslateRect(mainView, orig); NSRect frame = {{orig.x, orig.y}, {orig.width, orig.height}}; CefBrowserContentView* contentView = [[CefBrowserContentView alloc] initWithFrame:frame]; // Make the content view for the window have a layer. This will make all // sub-views have layers. This is necessary to ensure correct layer // ordering of all child views and their layers. [contentView setWantsLayer:YES]; [mainView addSubview:contentView]; [contentView setAutoresizingMask:(NSViewWidthSizable | NSViewHeightSizable)]; [contentView setNeedsDisplay:YES]; [contentView release]; // Override origin before "orig" is returned because the new origin is // relative to the created CefBrowserContentView object orig.x = 0; orig.y = 0; return contentView; } // translate java's window origin to Obj-C's window origin void TranslateRect(CefWindowHandle view, CefRect& orig) { NSRect bounds = [[[CAST_CEF_WINDOW_HANDLE_TO_NSVIEW(view) window] contentView] bounds]; orig.y = bounds.size.height - orig.height - orig.y; } bool CefInitializeOnMainThread(const CefMainArgs& args, const CefSettings& settings, CefRefPtr<ClientApp> application) { InitializeParams* params = [[InitializeParams alloc] init]; params->args_ = std::make_shared<CefMainArgs>(args); params->settings_ = settings; params->application_ = application; params->result_ = false; // Block until done. [JCEFThreadUtilities performOnMainThread:@selector(initialize:) on:[CefHandler class] withObject:params waitUntilDone:YES]; int result = params->result_; [params release]; return result; } void CefShutdownOnMainThread(void* tempWindow) { ShutdownParams* params = [[ShutdownParams alloc] init]; params->pointer_ = (TempWindowMac*)tempWindow; [[CefHandler class] performSelectorOnMainThread:@selector(shutdown:) withObject:params waitUntilDone:NO]; } void CefDoMessageLoopWorkOnMainThread() { [[CefHandler class] performSelectorOnMainThread:@selector(doMessageLoopWork) withObject:nil waitUntilDone:NO]; } void SetVisibility(CefWindowHandle handle, bool isVisible) { SetVisibilityParams* params = [[SetVisibilityParams alloc] init]; params->handle_ = handle; params->isVisible_ = isVisible; [[CefHandler class] performSelectorOnMainThread:@selector(setVisibility:) withObject:params waitUntilDone:NO]; } void UpdateView(CefWindowHandle handle, CefRect contentRect, CefRect browserRect) { if (!isBrowserExists(handle)) return; util_mac::TranslateRect(handle, contentRect); CefBrowserContentView* browser = (CefBrowserContentView*)[CAST_CEF_WINDOW_HANDLE_TO_NSVIEW(handle) superview]; browserRect.y = contentRect.height - browserRect.height - browserRect.y; // Only update the view if nobody is currently resizing the main window. // Otherwise the CefBrowser part may start flickering because there's a // significant delay between the native window resize event and the java // resize event if (![browser isLiveResizing]) { NSString* contentStr = [[NSString alloc] initWithFormat:@"{{%d,%d},{%d,%d}", contentRect.x, contentRect.y, contentRect.width, contentRect.height]; NSString* browserStr = [[NSString alloc] initWithFormat:@"{{%d,%d},{%d,%d}", browserRect.x, browserRect.y, browserRect.width, browserRect.height]; NSDictionary* dict = [[NSDictionary alloc] initWithObjectsAndKeys:contentStr, @"content", browserStr, @"browser", nil]; [browser performSelectorOnMainThread:@selector(updateView:) withObject:dict waitUntilDone:NO]; } } } // namespace util_mac namespace util { void AddCefBrowser(CefRefPtr<CefBrowser> browser) { if (!browser.get() || browser->GetHost()->IsWindowRenderingDisabled()) return; CefWindowHandle handle = browser->GetHost()->GetWindowHandle(); g_browsers_lock_.Lock(); g_browsers_.insert(handle); g_browsers_lock_.Unlock(); CefBrowserContentView* browserImpl = (CefBrowserContentView*)[CAST_CEF_WINDOW_HANDLE_TO_NSVIEW(handle) superview]; [browserImpl addCefBrowser:browser]; if (!g_mouse_monitor_) { [NSApplication setMouseMonitor]; } } void DestroyCefBrowser(CefRefPtr<CefBrowser> browser) { if (!browser.get() || browser->GetHost()->IsWindowRenderingDisabled()) return; NSView* handle = CAST_CEF_WINDOW_HANDLE_TO_NSVIEW(browser->GetHost()->GetWindowHandle()); g_browsers_lock_.Lock(); bool browser_exists = g_browsers_.erase(handle) > 0; g_browsers_lock_.Unlock(); if (!browser_exists) return; // There are some cases where the superview of CefBrowser isn't // a CefBrowserContentView. For example if another CefBrowser window was // created by calling "window.open()" in JavaScript. NSView* superView = [handle superview]; if ([superView isKindOfClass:[CefBrowserContentView class]]) { [(CefBrowserContentView*)superView destroyCefBrowser]; } } void SetParent(CefWindowHandle handle, jlong parentHandle, base::OnceClosure callback) { base::RepeatingClosure* pCallback = new base::RepeatingClosure( base::BindRepeating([](base::OnceClosure& cb) { std::move(cb).Run(); }, OwnedRef(std::move(callback)))); dispatch_async(dispatch_get_main_queue(), ^{ if (!isBrowserExists(handle)) return; CefBrowserContentView* browser_view = (CefBrowserContentView*)[CAST_CEF_WINDOW_HANDLE_TO_NSVIEW(handle) superview]; [browser_view retain]; [browser_view removeFromSuperview]; NSView* contentView; if (parentHandle) { NSWindow* window = (NSWindow*)parentHandle; contentView = [window contentView]; } else { contentView = CAST_CEF_WINDOW_HANDLE_TO_NSVIEW(TempWindow::GetWindowHandle()); } [contentView addSubview:browser_view]; [browser_view release]; pCallback->Run(); delete pCallback; }); } } // namespace util