1// Copyright (c) 2013 The Chromium Embedded Framework Authors. 2// Portions copyright (c) 2010 The Chromium Authors. All rights reserved. 3// Use of this source code is governed by a BSD-style license that can be 4// found in the LICENSE file. 5 6#import <Cocoa/Cocoa.h> 7#include "include/cef_app.h" 8#import "include/cef_application_mac.h" 9#import "include/wrapper/cef_library_loader.h" 10#include "tests/cefclient/browser/main_context_impl.h" 11#include "tests/cefclient/browser/resource.h" 12#include "tests/cefclient/browser/root_window.h" 13#include "tests/cefclient/browser/test_runner.h" 14#include "tests/shared/browser/client_app_browser.h" 15#include "tests/shared/browser/main_message_loop_external_pump.h" 16#include "tests/shared/browser/main_message_loop_std.h" 17#include "tests/shared/common/client_switches.h" 18 19namespace { 20 21// Returns the top menu bar with the specified |tag|. 22NSMenuItem* GetMenuBarMenuWithTag(NSInteger tag) { 23 NSMenu* main_menu = [[NSApplication sharedApplication] mainMenu]; 24 NSInteger found_index = [main_menu indexOfItemWithTag:tag]; 25 if (found_index >= 0) 26 return [main_menu itemAtIndex:found_index]; 27 return nil; 28} 29 30// Returns the item in |menu| that has the specified |action_selector|. 31NSMenuItem* GetMenuItemWithAction(NSMenu* menu, SEL action_selector) { 32 for (NSInteger i = 0; i < menu.numberOfItems; ++i) { 33 NSMenuItem* item = [menu itemAtIndex:i]; 34 if (item.action == action_selector) 35 return item; 36 } 37 return nil; 38} 39 40} // namespace 41 42// Receives notifications from the application. Will delete itself when done. 43@interface ClientAppDelegate : NSObject <NSApplicationDelegate> { 44 @private 45 bool with_controls_; 46 bool with_osr_; 47} 48 49- (id)initWithControls:(bool)with_controls andOsr:(bool)with_osr; 50- (void)createApplication:(id)object; 51- (void)tryToTerminateApplication:(NSApplication*)app; 52- (void)testsItemSelected:(int)command_id; 53- (IBAction)menuTestsGetText:(id)sender; 54- (IBAction)menuTestsGetSource:(id)sender; 55- (IBAction)menuTestsWindowNew:(id)sender; 56- (IBAction)menuTestsWindowPopup:(id)sender; 57- (IBAction)menuTestsRequest:(id)sender; 58- (IBAction)menuTestsPluginInfo:(id)sender; 59- (IBAction)menuTestsZoomIn:(id)sender; 60- (IBAction)menuTestsZoomOut:(id)sender; 61- (IBAction)menuTestsZoomReset:(id)sender; 62- (IBAction)menuTestsSetFPS:(id)sender; 63- (IBAction)menuTestsSetScaleFactor:(id)sender; 64- (IBAction)menuTestsTracingBegin:(id)sender; 65- (IBAction)menuTestsTracingEnd:(id)sender; 66- (IBAction)menuTestsPrint:(id)sender; 67- (IBAction)menuTestsPrintToPdf:(id)sender; 68- (IBAction)menuTestsMuteAudio:(id)sender; 69- (IBAction)menuTestsUnmuteAudio:(id)sender; 70- (IBAction)menuTestsOtherTests:(id)sender; 71- (void)enableAccessibility:(bool)bEnable; 72@end 73 74// Provide the CefAppProtocol implementation required by CEF. 75@interface ClientApplication : NSApplication <CefAppProtocol> { 76 @private 77 BOOL handlingSendEvent_; 78} 79@end 80 81@implementation ClientApplication 82 83- (BOOL)isHandlingSendEvent { 84 return handlingSendEvent_; 85} 86 87- (void)setHandlingSendEvent:(BOOL)handlingSendEvent { 88 handlingSendEvent_ = handlingSendEvent; 89} 90 91- (void)sendEvent:(NSEvent*)event { 92 CefScopedSendingEvent sendingEventScoper; 93 [super sendEvent:event]; 94} 95 96// |-terminate:| is the entry point for orderly "quit" operations in Cocoa. This 97// includes the application menu's quit menu item and keyboard equivalent, the 98// application's dock icon menu's quit menu item, "quit" (not "force quit") in 99// the Activity Monitor, and quits triggered by user logout and system restart 100// and shutdown. 101// 102// The default |-terminate:| implementation ends the process by calling exit(), 103// and thus never leaves the main run loop. This is unsuitable for Chromium 104// since Chromium depends on leaving the main run loop to perform an orderly 105// shutdown. We support the normal |-terminate:| interface by overriding the 106// default implementation. Our implementation, which is very specific to the 107// needs of Chromium, works by asking the application delegate to terminate 108// using its |-tryToTerminateApplication:| method. 109// 110// |-tryToTerminateApplication:| differs from the standard 111// |-applicationShouldTerminate:| in that no special event loop is run in the 112// case that immediate termination is not possible (e.g., if dialog boxes 113// allowing the user to cancel have to be shown). Instead, this method tries to 114// close all browsers by calling CloseBrowser(false) via 115// ClientHandler::CloseAllBrowsers. Calling CloseBrowser will result in a call 116// to ClientHandler::DoClose and execution of |-performClose:| on the NSWindow. 117// DoClose sets a flag that is used to differentiate between new close events 118// (e.g., user clicked the window close button) and in-progress close events 119// (e.g., user approved the close window dialog). The NSWindowDelegate 120// |-windowShouldClose:| method checks this flag and either calls 121// CloseBrowser(false) in the case of a new close event or destructs the 122// NSWindow in the case of an in-progress close event. 123// ClientHandler::OnBeforeClose will be called after the CEF NSView hosted in 124// the NSWindow is dealloc'ed. 125// 126// After the final browser window has closed ClientHandler::OnBeforeClose will 127// begin actual tear-down of the application by calling CefQuitMessageLoop. 128// This ends the NSApplication event loop and execution then returns to the 129// main() function for cleanup before application termination. 130// 131// The standard |-applicationShouldTerminate:| is not supported, and code paths 132// leading to it must be redirected. 133- (void)terminate:(id)sender { 134 ClientAppDelegate* delegate = static_cast<ClientAppDelegate*>( 135 [[NSApplication sharedApplication] delegate]); 136 [delegate tryToTerminateApplication:self]; 137 // Return, don't exit. The application is responsible for exiting on its own. 138} 139 140// Detect dynamically if VoiceOver is running. Like Chromium, rely upon the 141// undocumented accessibility attribute @"AXEnhancedUserInterface" which is set 142// when VoiceOver is launched and unset when VoiceOver is closed. 143- (void)accessibilitySetValue:(id)value forAttribute:(NSString*)attribute { 144 if ([attribute isEqualToString:@"AXEnhancedUserInterface"]) { 145 ClientAppDelegate* delegate = static_cast<ClientAppDelegate*>( 146 [[NSApplication sharedApplication] delegate]); 147 [delegate enableAccessibility:([value intValue] == 1)]; 148 } 149 return [super accessibilitySetValue:value forAttribute:attribute]; 150} 151@end 152 153@implementation ClientAppDelegate 154 155- (id)initWithControls:(bool)with_controls andOsr:(bool)with_osr { 156 if (self = [super init]) { 157 with_controls_ = with_controls; 158 with_osr_ = with_osr; 159 } 160 return self; 161} 162 163// Create the application on the UI thread. 164- (void)createApplication:(id)object { 165 NSApplication* application = [NSApplication sharedApplication]; 166 167 // The top menu is configured using Interface Builder (IB). To modify the menu 168 // start by loading MainMenu.xib in IB. 169 // 170 // To associate MainMenu.xib with ClientAppDelegate: 171 // 1. Select "File's Owner" from the "Placeholders" section in the left side 172 // pane. 173 // 2. Load the "Identity inspector" tab in the top-right side pane. 174 // 3. In the "Custom Class" section set the "Class" value to 175 // "ClientAppDelegate". 176 // 4. Pass an instance of ClientAppDelegate as the |owner| parameter to 177 // loadNibNamed:. 178 // 179 // To create a new top menu: 180 // 1. Load the "Object library" tab in the bottom-right side pane. 181 // 2. Drag a "Submenu Menu Item" widget from the Object library to the desired 182 // location in the menu bar shown in the center pane. 183 // 3. Select the newly created top menu by left clicking on it. 184 // 4. Load the "Attributes inspector" tab in the top-right side pane. 185 // 5. Under the "Menu Item" section set the "Tag" value to a unique integer. 186 // This is necessary for the GetMenuBarMenuWithTag function to work 187 // properly. 188 // 189 // To create a new menu item in a top menu: 190 // 1. Add a new receiver method in ClientAppDelegate (e.g. menuTestsDoStuff:). 191 // 2. Load the "Object library" tab in the bottom-right side pane. 192 // 3. Drag a "Menu Item" widget from the Object library to the desired 193 // location in the menu bar shown in the center pane. 194 // 4. Double-click on the new menu item to set the label. 195 // 5. Right click on the new menu item to show the "Get Source" dialog. 196 // 6. In the "Sent Actions" section drag from the circle icon and drop on the 197 // new receiver method in the ClientAppDelegate source code file. 198 // 199 // Load the top menu from MainMenu.xib. 200 [[NSBundle mainBundle] loadNibNamed:@"MainMenu" 201 owner:self 202 topLevelObjects:nil]; 203 204 // Set the delegate for application events. 205 [application setDelegate:self]; 206 207 if (!with_osr_) { 208 // Remove the OSR-related menu items when OSR is disabled. 209 NSMenuItem* tests_menu = GetMenuBarMenuWithTag(8); 210 if (tests_menu) { 211 NSMenuItem* set_fps_item = GetMenuItemWithAction( 212 tests_menu.submenu, @selector(menuTestsSetFPS:)); 213 if (set_fps_item) 214 [tests_menu.submenu removeItem:set_fps_item]; 215 NSMenuItem* set_scale_factor_item = GetMenuItemWithAction( 216 tests_menu.submenu, @selector(menuTestsSetScaleFactor:)); 217 if (set_scale_factor_item) 218 [tests_menu.submenu removeItem:set_scale_factor_item]; 219 } 220 } 221 222 auto window_config = std::make_unique<client::RootWindowConfig>(); 223 window_config->with_controls = with_controls_; 224 window_config->with_osr = with_osr_; 225 226 // Create the first window. 227 client::MainContext::Get()->GetRootWindowManager()->CreateRootWindow( 228 std::move(window_config)); 229} 230 231- (void)tryToTerminateApplication:(NSApplication*)app { 232 client::MainContext::Get()->GetRootWindowManager()->CloseAllWindows(false); 233} 234 235- (void)orderFrontStandardAboutPanel:(id)sender { 236 [[NSApplication sharedApplication] orderFrontStandardAboutPanel:nil]; 237} 238 239- (void)testsItemSelected:(int)command_id { 240 // Retrieve the active RootWindow. 241 NSWindow* key_window = [[NSApplication sharedApplication] keyWindow]; 242 if (!key_window) 243 return; 244 245 scoped_refptr<client::RootWindow> root_window = 246 client::RootWindow::GetForNSWindow(key_window); 247 248 CefRefPtr<CefBrowser> browser = root_window->GetBrowser(); 249 if (browser.get()) 250 client::test_runner::RunTest(browser, command_id); 251} 252 253- (IBAction)menuTestsGetText:(id)sender { 254 [self testsItemSelected:ID_TESTS_GETTEXT]; 255} 256 257- (IBAction)menuTestsGetSource:(id)sender { 258 [self testsItemSelected:ID_TESTS_GETSOURCE]; 259} 260 261- (IBAction)menuTestsWindowNew:(id)sender { 262 [self testsItemSelected:ID_TESTS_WINDOW_NEW]; 263} 264 265- (IBAction)menuTestsWindowPopup:(id)sender { 266 [self testsItemSelected:ID_TESTS_WINDOW_POPUP]; 267} 268 269- (IBAction)menuTestsRequest:(id)sender { 270 [self testsItemSelected:ID_TESTS_REQUEST]; 271} 272 273- (IBAction)menuTestsPluginInfo:(id)sender { 274 [self testsItemSelected:ID_TESTS_PLUGIN_INFO]; 275} 276 277- (IBAction)menuTestsZoomIn:(id)sender { 278 [self testsItemSelected:ID_TESTS_ZOOM_IN]; 279} 280 281- (IBAction)menuTestsZoomOut:(id)sender { 282 [self testsItemSelected:ID_TESTS_ZOOM_OUT]; 283} 284 285- (IBAction)menuTestsZoomReset:(id)sender { 286 [self testsItemSelected:ID_TESTS_ZOOM_RESET]; 287} 288 289- (IBAction)menuTestsSetFPS:(id)sender { 290 [self testsItemSelected:ID_TESTS_OSR_FPS]; 291} 292 293- (IBAction)menuTestsSetScaleFactor:(id)sender { 294 [self testsItemSelected:ID_TESTS_OSR_DSF]; 295} 296 297- (IBAction)menuTestsTracingBegin:(id)sender { 298 [self testsItemSelected:ID_TESTS_TRACING_BEGIN]; 299} 300 301- (IBAction)menuTestsTracingEnd:(id)sender { 302 [self testsItemSelected:ID_TESTS_TRACING_END]; 303} 304 305- (IBAction)menuTestsPrint:(id)sender { 306 [self testsItemSelected:ID_TESTS_PRINT]; 307} 308 309- (IBAction)menuTestsPrintToPdf:(id)sender { 310 [self testsItemSelected:ID_TESTS_PRINT_TO_PDF]; 311} 312 313- (IBAction)menuTestsMuteAudio:(id)sender { 314 [self testsItemSelected:ID_TESTS_MUTE_AUDIO]; 315} 316 317- (IBAction)menuTestsUnmuteAudio:(id)sender { 318 [self testsItemSelected:ID_TESTS_UNMUTE_AUDIO]; 319} 320 321- (IBAction)menuTestsOtherTests:(id)sender { 322 [self testsItemSelected:ID_TESTS_OTHER_TESTS]; 323} 324 325- (void)enableAccessibility:(bool)bEnable { 326 // Retrieve the active RootWindow. 327 NSWindow* key_window = [[NSApplication sharedApplication] keyWindow]; 328 if (!key_window) 329 return; 330 331 scoped_refptr<client::RootWindow> root_window = 332 client::RootWindow::GetForNSWindow(key_window); 333 334 CefRefPtr<CefBrowser> browser = root_window->GetBrowser(); 335 if (browser.get()) { 336 browser->GetHost()->SetAccessibilityState(bEnable ? STATE_ENABLED 337 : STATE_DISABLED); 338 } 339} 340 341- (NSApplicationTerminateReply)applicationShouldTerminate: 342 (NSApplication*)sender { 343 return NSTerminateNow; 344} 345 346@end 347 348namespace client { 349namespace { 350 351int RunMain(int argc, char* argv[]) { 352 // Load the CEF framework library at runtime instead of linking directly 353 // as required by the macOS sandbox implementation. 354 CefScopedLibraryLoader library_loader; 355 if (!library_loader.LoadInMain()) 356 return 1; 357 358 int result = -1; 359 360 CefMainArgs main_args(argc, argv); 361 362 @autoreleasepool { 363 // Initialize the ClientApplication instance. 364 [ClientApplication sharedApplication]; 365 366 // If there was an invocation to NSApp prior to this method, then the NSApp 367 // will not be a ClientApplication, but will instead be an NSApplication. 368 // This is undesirable and we must enforce that this doesn't happen. 369 CHECK([NSApp isKindOfClass:[ClientApplication class]]); 370 371 // Parse command-line arguments. 372 CefRefPtr<CefCommandLine> command_line = 373 CefCommandLine::CreateCommandLine(); 374 command_line->InitFromArgv(argc, argv); 375 376 // Create a ClientApp of the correct type. 377 CefRefPtr<CefApp> app; 378 ClientApp::ProcessType process_type = 379 ClientApp::GetProcessType(command_line); 380 if (process_type == ClientApp::BrowserProcess) 381 app = new ClientAppBrowser(); 382 383 // Create the main context object. 384 std::unique_ptr<MainContextImpl> context( 385 new MainContextImpl(command_line, true)); 386 387 CefSettings settings; 388 389// When generating projects with CMake the CEF_USE_SANDBOX value will be defined 390// automatically. Pass -DUSE_SANDBOX=OFF to the CMake command-line to disable 391// use of the sandbox. 392#if !defined(CEF_USE_SANDBOX) 393 settings.no_sandbox = true; 394#endif 395 396 // Populate the settings based on command line arguments. 397 context->PopulateSettings(&settings); 398 399 // Create the main message loop object. 400 std::unique_ptr<MainMessageLoop> message_loop; 401 if (settings.external_message_pump) 402 message_loop = MainMessageLoopExternalPump::Create(); 403 else 404 message_loop.reset(new MainMessageLoopStd); 405 406 // Initialize CEF. 407 context->Initialize(main_args, settings, app, nullptr); 408 409 // Register scheme handlers. 410 test_runner::RegisterSchemeHandlers(); 411 412 // Create the application delegate and window. 413 ClientAppDelegate* delegate = [[ClientAppDelegate alloc] 414 initWithControls:!command_line->HasSwitch(switches::kHideControls) 415 andOsr:settings.windowless_rendering_enabled ? true : false]; 416 [delegate performSelectorOnMainThread:@selector(createApplication:) 417 withObject:nil 418 waitUntilDone:NO]; 419 420 // Run the message loop. This will block until Quit() is called. 421 result = message_loop->Run(); 422 423 // Shut down CEF. 424 context->Shutdown(); 425 426 // Release objects in reverse order of creation. 427#if !__has_feature(objc_arc) 428 [delegate release]; 429#endif // !__has_feature(objc_arc) 430 delegate = nil; 431 message_loop.reset(); 432 context.reset(); 433 } // @autoreleasepool 434 435 return result; 436} 437 438} // namespace 439} // namespace client 440 441// Entry point function for the browser process. 442int main(int argc, char* argv[]) { 443 return client::RunMain(argc, argv); 444} 445