blob: ceea5c260c6167847602c8ad16ff648d1a8c8951 [file] [log] [blame]
[email protected]ce04f0c92010-03-03 19:27:281// Copyright (c) 2010 The Chromium Authors. All rights reserved.
[email protected]e2ac70002008-12-09 14:58:132// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "base/mac_util.h"
6
7#import <Cocoa/Cocoa.h>
8
[email protected]6f33add32009-03-03 16:26:039#include "base/file_path.h"
10#include "base/logging.h"
[email protected]b44dbd12009-10-11 19:02:1511#include "base/message_loop.h"
[email protected]e2ac70002008-12-09 14:58:1312#include "base/scoped_cftyperef.h"
[email protected]6f33add32009-03-03 16:26:0313#include "base/sys_string_conversions.h"
[email protected]e2ac70002008-12-09 14:58:1314
[email protected]ce04f0c92010-03-03 19:27:2815namespace {
16
17// a count of currently outstanding requests for full screen mode from browser
18// windows, plugins, etc.
19int g_full_screen_requests[mac_util::kNumFullScreenModes] = { 0, 0, 0};
20
21// Sets the appropriate SystemUIMode based on the current full screen requests.
22// Since only one SystemUIMode can be active at a given time, full screen
23// requests are ordered by priority. If there are no outstanding full screen
24// requests, reverts to normal mode. If the correct SystemUIMode is already
25// set, does nothing.
26void SetUIMode() {
27 // Get the current UI mode.
28 SystemUIMode current_mode;
29 GetSystemUIMode(&current_mode, NULL);
30
31 // Determine which mode should be active, based on which requests are
32 // currently outstanding. More permissive requests take precedence. For
33 // example, plugins request |kFullScreenModeAutoHideAll|, while browser
34 // windows request |kFullScreenModeHideDock| when the fullscreen overlay is
35 // down. Precedence goes to plugins in this case, so AutoHideAll wins over
36 // HideDock.
37 SystemUIMode desired_mode = kUIModeNormal;
38 SystemUIOptions desired_options = 0;
39 if (g_full_screen_requests[mac_util::kFullScreenModeAutoHideAll] > 0) {
40 desired_mode = kUIModeAllHidden;
41 desired_options = kUIOptionAutoShowMenuBar;
42 } else if (g_full_screen_requests[mac_util::kFullScreenModeHideDock] > 0) {
43 desired_mode = kUIModeContentHidden;
44 } else if (g_full_screen_requests[mac_util::kFullScreenModeHideAll] > 0) {
45 desired_mode = kUIModeAllHidden;
46 }
47
48 if (current_mode != desired_mode)
49 SetSystemUIMode(desired_mode, desired_options);
50}
51
52} // end namespace
53
[email protected]e2ac70002008-12-09 14:58:1354namespace mac_util {
55
56std::string PathFromFSRef(const FSRef& ref) {
57 scoped_cftyperef<CFURLRef> url(
58 CFURLCreateFromFSRef(kCFAllocatorDefault, &ref));
59 NSString *path_string = [(NSURL *)url.get() path];
60 return [path_string fileSystemRepresentation];
61}
62
63bool FSRefFromPath(const std::string& path, FSRef* ref) {
64 OSStatus status = FSPathMakeRef((const UInt8*)path.c_str(),
65 ref, nil);
66 return status == noErr;
67}
68
[email protected]480e1292008-12-10 22:05:4069// Adapted from http://developer.apple.com/carbon/tipsandtricks.html#AmIBundled
70bool AmIBundled() {
71 ProcessSerialNumber psn = {0, kCurrentProcess};
72
73 FSRef fsref;
74 if (GetProcessBundleLocation(&psn, &fsref) != noErr)
75 return false;
76
77 FSCatalogInfo info;
78 if (FSGetCatalogInfo(&fsref, kFSCatInfoNodeFlags, &info,
79 NULL, NULL, NULL) != noErr) {
80 return false;
81 }
82
83 return info.nodeFlags & kFSNodeIsDirectoryMask;
84}
85
[email protected]385f4972009-08-31 23:08:0086bool IsBackgroundOnlyProcess() {
[email protected]7e2668792009-09-03 14:14:0987 // This function really does want to examine NSBundle's idea of the main
88 // bundle dictionary, and not the overriden MainAppBundle. It needs to look
89 // at the actual running .app's Info.plist to access its LSUIElement
90 // property.
91 NSDictionary* info_dictionary = [[NSBundle mainBundle] infoDictionary];
[email protected]a9b1f172009-09-01 16:55:5992 return [[info_dictionary objectForKey:@"LSUIElement"] boolValue] != NO;
[email protected]385f4972009-08-31 23:08:0093}
94
[email protected]6f33add32009-03-03 16:26:0395// No threading worries since NSBundle isn't thread safe.
96static NSBundle* g_override_app_bundle = nil;
97
98NSBundle* MainAppBundle() {
99 if (g_override_app_bundle)
100 return g_override_app_bundle;
101 return [NSBundle mainBundle];
102}
103
[email protected]e4221ad52009-10-07 16:42:16104FilePath MainAppBundlePath() {
105 NSBundle* bundle = MainAppBundle();
106 return FilePath([[bundle bundlePath] fileSystemRepresentation]);
107}
108
[email protected]4b77bf12009-08-11 21:27:30109void SetOverrideAppBundle(NSBundle* bundle) {
110 if (bundle != g_override_app_bundle) {
111 [g_override_app_bundle release];
112 g_override_app_bundle = [bundle retain];
113 }
114}
115
116void SetOverrideAppBundlePath(const FilePath& file_path) {
117 NSString* path = base::SysUTF8ToNSString(file_path.value());
118 NSBundle* bundle = [NSBundle bundleWithPath:path];
[email protected]2b54eeb2009-10-19 20:05:50119 CHECK(bundle) << "Failed to load the bundle at " << file_path.value();
[email protected]4b77bf12009-08-11 21:27:30120
121 SetOverrideAppBundle(bundle);
122}
123
124OSType CreatorCodeForCFBundleRef(CFBundleRef bundle) {
[email protected]0f7bc442009-08-12 16:25:45125 OSType creator = kUnknownType;
[email protected]4b77bf12009-08-11 21:27:30126 CFBundleGetPackageInfo(bundle, NULL, &creator);
127 return creator;
128}
129
130OSType CreatorCodeForApplication() {
131 CFBundleRef bundle = CFBundleGetMainBundle();
132 if (!bundle)
133 return kUnknownType;
134
135 return CreatorCodeForCFBundleRef(bundle);
136}
137
[email protected]2438b1722010-01-27 01:27:19138bool GetUserDirectory(NSSearchPathDirectory directory, FilePath* result) {
139 DCHECK(result);
140 NSArray* dirs =
141 NSSearchPathForDirectoriesInDomains(directory, NSUserDomainMask, YES);
142 if ([dirs count] < 1) {
143 return false;
144 }
145 NSString* path = [dirs objectAtIndex:0];
146 *result = FilePath([path fileSystemRepresentation]);
147 return true;
148}
149
[email protected]4dfae512009-07-31 20:18:49150FilePath GetUserLibraryPath() {
[email protected]2438b1722010-01-27 01:27:19151 FilePath user_library_path;
152 if (!GetUserDirectory(NSLibraryDirectory, &user_library_path)) {
153 LOG(WARNING) << "Could not get user library path";
154 }
155 return user_library_path;
[email protected]4dfae512009-07-31 20:18:49156}
157
[email protected]265cf96b2009-09-03 21:51:10158CGColorSpaceRef GetSRGBColorSpace() {
159 // Leaked. That's OK, it's scoped to the lifetime of the application.
160 static CGColorSpaceRef g_color_space_sRGB =
161 CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
[email protected]2b7c2bf2009-09-13 15:01:13162 LOG_IF(ERROR, !g_color_space_sRGB) << "Couldn't get the sRGB color space";
[email protected]265cf96b2009-09-03 21:51:10163 return g_color_space_sRGB;
164}
165
166CGColorSpaceRef GetSystemColorSpace() {
167 // Leaked. That's OK, it's scoped to the lifetime of the application.
[email protected]2b7c2bf2009-09-13 15:01:13168 // Try to get the main display's color space.
169 static CGColorSpaceRef g_system_color_space =
170 CGDisplayCopyColorSpace(CGMainDisplayID());
[email protected]265cf96b2009-09-03 21:51:10171
172 if (!g_system_color_space) {
[email protected]2b7c2bf2009-09-13 15:01:13173 // Use a generic RGB color space. This is better than nothing.
174 g_system_color_space = CGColorSpaceCreateDeviceRGB();
175
176 if (g_system_color_space) {
177 LOG(WARNING) <<
178 "Couldn't get the main display's color space, using generic";
179 } else {
180 LOG(ERROR) << "Couldn't get any color space";
[email protected]265cf96b2009-09-03 21:51:10181 }
182 }
[email protected]2b7c2bf2009-09-13 15:01:13183
[email protected]265cf96b2009-09-03 21:51:10184 return g_system_color_space;
185}
[email protected]6f33add32009-03-03 16:26:03186
[email protected]ce04f0c92010-03-03 19:27:28187// Add a request for full screen mode. Must be called on the main thread.
188void RequestFullScreen(FullScreenMode mode) {
189 DCHECK_LT(mode, kNumFullScreenModes);
190 if (mode >= kNumFullScreenModes)
191 return;
[email protected]b44dbd12009-10-11 19:02:15192
[email protected]ce04f0c92010-03-03 19:27:28193 DCHECK_GE(g_full_screen_requests[mode], 0);
194 g_full_screen_requests[mode] = std::max(g_full_screen_requests[mode] + 1, 1);
195 SetUIMode();
[email protected]b44dbd12009-10-11 19:02:15196}
197
[email protected]ce04f0c92010-03-03 19:27:28198// Release a request for full screen mode. Must be called on the main thread.
199void ReleaseFullScreen(FullScreenMode mode) {
200 DCHECK_LT(mode, kNumFullScreenModes);
201 if (mode >= kNumFullScreenModes)
202 return;
203
204 DCHECK_GT(g_full_screen_requests[mode], 0);
205 g_full_screen_requests[mode] = std::max(g_full_screen_requests[mode] - 1, 0);
206 SetUIMode();
207}
208
209// Switches full screen modes. Releases a request for |from_mode| and adds a
210// new request for |to_mode|. Must be called on the main thread.
211void SwitchFullScreenModes(FullScreenMode from_mode, FullScreenMode to_mode) {
212 DCHECK_LT(from_mode, kNumFullScreenModes);
213 DCHECK_LT(to_mode, kNumFullScreenModes);
214 if (from_mode >= kNumFullScreenModes || to_mode >= kNumFullScreenModes)
215 return;
216
217 DCHECK_GT(g_full_screen_requests[from_mode], 0);
218 DCHECK_GE(g_full_screen_requests[to_mode], 0);
219 g_full_screen_requests[from_mode] =
220 std::max(g_full_screen_requests[from_mode] - 1, 0);
221 g_full_screen_requests[to_mode] =
222 std::max(g_full_screen_requests[to_mode] + 1, 1);
223 SetUIMode();
[email protected]b44dbd12009-10-11 19:02:15224}
225
[email protected]18db46182010-02-02 17:04:55226void SetCursorVisibility(bool visible) {
227 if (visible)
228 [NSCursor unhide];
229 else
230 [NSCursor hide];
231}
232
[email protected]8050f2a2010-03-12 07:28:17233bool ShouldWindowsMiniaturizeOnDoubleClick() {
234 // We use an undocumented method in Cocoa; if it doesn't exist, default to
235 // |true|. If it ever goes away, we can do (using an undocumented pref key):
236 // NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
237 // return ![defaults objectForKey:@"AppleMiniaturizeOnDoubleClick"] ||
238 // [defaults boolForKey:@"AppleMiniaturizeOnDoubleClick"];
239 BOOL methodImplemented =
240 [NSWindow respondsToSelector:@selector(_shouldMiniaturizeOnDoubleClick)];
241 DCHECK(methodImplemented);
242 return !methodImplemented ||
243 [NSWindow performSelector:@selector(_shouldMiniaturizeOnDoubleClick)];
244}
245
[email protected]8b386ced2009-10-27 15:55:52246void GrabWindowSnapshot(NSWindow* window,
[email protected]86206622010-05-14 05:45:26247 std::vector<unsigned char>* png_representation,
248 int* width, int* height) {
[email protected]8b386ced2009-10-27 15:55:52249 // Make sure to grab the "window frame" view so we get current tab +
250 // tabstrip.
251 NSView* view = [[window contentView] superview];
252 NSBitmapImageRep* rep =
253 [view bitmapImageRepForCachingDisplayInRect:[view bounds]];
254 [view cacheDisplayInRect:[view bounds] toBitmapImageRep:rep];
255 NSData* data = [rep representationUsingType:NSPNGFileType properties:nil];
256 const unsigned char* buf = static_cast<const unsigned char*>([data bytes]);
257 NSUInteger length = [data length];
258 if (buf != NULL && length > 0){
[email protected]86206622010-05-14 05:45:26259 *width = static_cast<int>([rep pixelsWide]);
260 *height = static_cast<int>([rep pixelsHigh]);
[email protected]8b386ced2009-10-27 15:55:52261 png_representation->assign(buf, buf + length);
262 DCHECK(png_representation->size() > 0);
263 }
264}
265
[email protected]a96ec6a2009-11-04 17:27:08266void ActivateProcess(pid_t pid) {
267 ProcessSerialNumber process;
268 OSStatus status = GetProcessForPID(pid, &process);
269 if (status == noErr) {
270 SetFrontProcess(&process);
271 } else {
272 LOG(WARNING) << "Unable to get process for pid " << pid;
273 }
274}
275
[email protected]f164cea2009-11-05 23:37:40276// Takes a path to an (executable) binary and tries to provide the path to an
277// application bundle containing it. It takes the outermost bundle that it can
278// find (so for "/Foo/Bar.app/.../Baz.app/..." it produces "/Foo/Bar.app").
279// |exec_name| - path to the binary
280// returns - path to the application bundle, or empty on error
281FilePath GetAppBundlePath(const FilePath& exec_name) {
282 const char kExt[] = ".app";
283 const size_t kExtLength = arraysize(kExt) - 1;
284
285 // Split the path into components.
286 std::vector<std::string> components;
287 exec_name.GetComponents(&components);
288
289 // It's an error if we don't get any components.
290 if (!components.size())
291 return FilePath();
292
293 // Don't prepend '/' to the first component.
294 std::vector<std::string>::const_iterator it = components.begin();
295 std::string bundle_name = *it;
296 DCHECK(it->length() > 0);
297 // If the first component ends in ".app", we're already done.
298 if (it->length() > kExtLength &&
299 !it->compare(it->length() - kExtLength, kExtLength, kExt, kExtLength))
300 return FilePath(bundle_name);
301
302 // The first component may be "/" or "//", etc. Only append '/' if it doesn't
303 // already end in '/'.
304 if (bundle_name[bundle_name.length() - 1] != '/')
305 bundle_name += '/';
306
307 // Go through the remaining components.
308 for (++it; it != components.end(); ++it) {
309 DCHECK(it->length() > 0);
310
311 bundle_name += *it;
312
313 // If the current component ends in ".app", we're done.
314 if (it->length() > kExtLength &&
315 !it->compare(it->length() - kExtLength, kExtLength, kExt, kExtLength))
316 return FilePath(bundle_name);
317
318 // Separate this component from the next one.
319 bundle_name += '/';
320 }
321
322 return FilePath();
323}
324
[email protected]b4113d52009-11-11 02:49:05325bool SetFileBackupExclusion(const FilePath& file_path, bool exclude) {
326 NSString* filePath =
327 [NSString stringWithUTF8String:file_path.value().c_str()];
[email protected]254e0432009-12-30 17:41:14328
[email protected]edd96f52009-12-30 22:13:20329 // If being asked to exclude something in a tmp directory, just lie and say it
330 // was done. TimeMachine will already ignore tmp directories. This keeps the
331 // temporary profiles used by unittests from being added to the exclude list.
332 // Otherwise, as /Library/Preferences/com.apple.TimeMachine.plist grows the
333 // bots slow down due to reading/writing all the temporary profiles used over
334 // time.
335
336 NSString* tmpDir = NSTemporaryDirectory();
337 // Make sure the temp dir is terminated with a slash
338 if (tmpDir && ![tmpDir hasSuffix:@"/"])
339 tmpDir = [tmpDir stringByAppendingString:@"/"];
340 // '/var' is a link to '/private/var', make sure to check both forms.
341 NSString* privateTmpDir = nil;
342 if ([tmpDir hasPrefix:@"/var/"])
343 privateTmpDir = [@"/private" stringByAppendingString:tmpDir];
344
345 if ((tmpDir && [filePath hasPrefix:tmpDir]) ||
346 (privateTmpDir && [filePath hasPrefix:privateTmpDir]) ||
347 [filePath hasPrefix:@"/tmp/"] ||
[email protected]254e0432009-12-30 17:41:14348 [filePath hasPrefix:@"/var/tmp/"] ||
349 [filePath hasPrefix:@"/private/tmp/"] ||
350 [filePath hasPrefix:@"/private/var/tmp/"]) {
351 return true;
352 }
353
[email protected]b4113d52009-11-11 02:49:05354 NSURL* url = [NSURL fileURLWithPath:filePath];
355 // Note that we always set CSBackupSetItemExcluded's excludeByPath param
356 // to true. This prevents a problem with toggling the setting: if the file
357 // is excluded with excludeByPath set to true then excludeByPath must
358 // also be true when un-excluding the file, otherwise the un-excluding
359 // will be ignored.
360 bool success =
361 CSBackupSetItemExcluded((CFURLRef)url, exclude, true) == noErr;
362 if (!success)
363 LOG(WARNING) << "Failed to set backup excluson for file '"
364 << file_path.value().c_str() << "'. Continuing.";
365 return success;
366}
367
[email protected]a9944f02009-12-10 10:37:27368CFTypeRef GetValueFromDictionary(CFDictionaryRef dict,
369 CFStringRef key,
370 CFTypeID expected_type) {
371 CFTypeRef value = CFDictionaryGetValue(dict, key);
372 if (!value)
373 return value;
374
375 if (CFGetTypeID(value) != expected_type) {
376 scoped_cftyperef<CFStringRef> expected_type_ref(
377 CFCopyTypeIDDescription(expected_type));
378 scoped_cftyperef<CFStringRef> actual_type_ref(
379 CFCopyTypeIDDescription(CFGetTypeID(value)));
380 LOG(WARNING) << "Expected value for key "
381 << base::SysCFStringRefToUTF8(key)
382 << " to be "
383 << base::SysCFStringRefToUTF8(expected_type_ref)
384 << " but it was "
385 << base::SysCFStringRefToUTF8(actual_type_ref)
386 << " instead";
387 return NULL;
388 }
389
390 return value;
391}
[email protected]b4113d52009-11-11 02:49:05392
[email protected]0070eab2010-02-24 23:46:39393void SetProcessName(CFStringRef process_name) {
[email protected]13b290e72010-04-07 19:56:11394 if (!process_name || CFStringGetLength(process_name) == 0) {
395 NOTREACHED() << "SetProcessName given bad name.";
396 return;
397 }
398
399 if (![NSThread isMainThread]) {
400 NOTREACHED() << "Should only set process name from main thread.";
401 return;
402 }
403
[email protected]0070eab2010-02-24 23:46:39404 // Warning: here be dragons! This is SPI reverse-engineered from WebKit's
405 // plugin host, and could break at any time (although realistically it's only
406 // likely to break in a new major release).
407 // When 10.7 is available, check that this still works, and update this
408 // comment for 10.8.
409
410 // Private CFType used in these LaunchServices calls.
411 typedef CFTypeRef PrivateLSASN;
412 typedef PrivateLSASN (*LSGetCurrentApplicationASNType)();
413 typedef OSStatus (*LSSetApplicationInformationItemType)(int, PrivateLSASN,
414 CFStringRef,
415 CFStringRef,
416 CFDictionaryRef*);
417
418 static LSGetCurrentApplicationASNType ls_get_current_application_asn_func =
419 NULL;
420 static LSSetApplicationInformationItemType
421 ls_set_application_information_item_func = NULL;
422 static CFStringRef ls_display_name_key = NULL;
423
424 static bool did_symbol_lookup = false;
425 if (!did_symbol_lookup) {
426 did_symbol_lookup = true;
427 CFBundleRef launch_services_bundle =
428 CFBundleGetBundleWithIdentifier(CFSTR("com.apple.LaunchServices"));
429 if (!launch_services_bundle) {
430 LOG(ERROR) << "Failed to look up LaunchServices bundle";
431 return;
432 }
433
434 ls_get_current_application_asn_func =
435 reinterpret_cast<LSGetCurrentApplicationASNType>(
436 CFBundleGetFunctionPointerForName(
437 launch_services_bundle, CFSTR("_LSGetCurrentApplicationASN")));
438 if (!ls_get_current_application_asn_func)
439 LOG(ERROR) << "Could not find _LSGetCurrentApplicationASN";
440
441 ls_set_application_information_item_func =
442 reinterpret_cast<LSSetApplicationInformationItemType>(
443 CFBundleGetFunctionPointerForName(
444 launch_services_bundle,
445 CFSTR("_LSSetApplicationInformationItem")));
446 if (!ls_set_application_information_item_func)
447 LOG(ERROR) << "Could not find _LSSetApplicationInformationItem";
448
449 const CFStringRef* key_pointer = reinterpret_cast<const CFStringRef*>(
450 CFBundleGetDataPointerForName(launch_services_bundle,
451 CFSTR("_kLSDisplayNameKey")));
452 ls_display_name_key = key_pointer ? *key_pointer : NULL;
453 if (!ls_display_name_key)
454 LOG(ERROR) << "Could not find _kLSDisplayNameKey";
[email protected]1e23bac2010-04-12 21:08:47455
456 // Internally, this call relies on the Mach ports that are started up by the
457 // Carbon Process Manager. In debug builds this usually happens due to how
458 // the logging layers are started up; but in release, it isn't started in as
459 // much of a defined order. So if the symbols had to be loaded, go ahead
460 // and force a call to make sure the manager has been initialized and hence
461 // the ports are opened.
462 ProcessSerialNumber psn;
463 GetCurrentProcess(&psn);
[email protected]0070eab2010-02-24 23:46:39464 }
465 if (!ls_get_current_application_asn_func ||
466 !ls_set_application_information_item_func ||
467 !ls_display_name_key) {
468 return;
469 }
470
471 PrivateLSASN asn = ls_get_current_application_asn_func();
472 // Constant used by WebKit; what exactly it means is unknown.
473 const int magic_session_constant = -2;
[email protected]13b290e72010-04-07 19:56:11474 OSErr err =
475 ls_set_application_information_item_func(magic_session_constant, asn,
476 ls_display_name_key,
477 process_name,
478 NULL /* optional out param */);
479 LOG_IF(ERROR, err) << "Call to set process name failed, err " << err;
[email protected]0070eab2010-02-24 23:46:39480}
481
[email protected]8e953362010-07-28 19:38:50482// Converts a NSImage to a CGImageRef. Normally, the system frameworks can do
483// this fine, especially on 10.6. On 10.5, however, CGImage cannot handle
484// converting a PDF-backed NSImage into a CGImageRef. This function will
485// rasterize the PDF into a bitmap CGImage. The caller is responsible for
486// releasing the return value.
487CGImageRef CopyNSImageToCGImage(NSImage* image) {
488 // This is based loosely on http://www.cocoadev.com/index.pl?CGImageRef .
489 NSSize size = [image size];
490 scoped_cftyperef<CGContextRef> context(
491 CGBitmapContextCreate(NULL, // Allow CG to allocate memory.
492 size.width,
493 size.height,
494 8, // bitsPerComponent
495 0, // bytesPerRow - CG will calculate by default.
496 [[NSColorSpace genericRGBColorSpace] CGColorSpace],
497 kCGBitmapByteOrder32Host |
498 kCGImageAlphaPremultipliedFirst));
499 if (!context.get())
500 return NULL;
501
502 [NSGraphicsContext saveGraphicsState];
503 [NSGraphicsContext setCurrentContext:
504 [NSGraphicsContext graphicsContextWithGraphicsPort:context.get()
505 flipped:NO]];
506 [image drawInRect:NSMakeRect(0,0, size.width, size.height)
507 fromRect:NSZeroRect
508 operation:NSCompositeCopy
509 fraction:1.0];
510 [NSGraphicsContext restoreGraphicsState];
511
512 return CGBitmapContextCreateImage(context);
513}
514
[email protected]e2ac70002008-12-09 14:58:13515} // namespace mac_util