What's is a reliable way to detect if user has enabled this API?
CGWindowListCreateImage returns a valid object even if screen recording API is disabled. There are multiple combinations possible (kCGWindowListOptionIncludingWindow, kCGWindowListOptionOnScreenBelowWindow) and only some will return NULL.
- (CGImageRef)createScreenshotImage
{
NSWindow *window = [[self view] window];
NSRect rect = [window frame];
rect.origin.y = NSHeight([[window screen] frame]) - NSMaxY([window frame]);
CGImageRef screenshot = CGWindowListCreateImage(
rect,
kCGWindowListOptionIncludingWindow,
//kCGWindowListOptionOnScreenBelowWindow,
0,//(CGWindowID)[window windowNumber],
kCGWindowImageBoundsIgnoreFraming);//kCGWindowImageDefault
return screenshot;
}
The only reliable way is through CGDisplayStreamCreate which is risky as Apple always changes privacy settings every year.
- (BOOL)canRecordScreen
{
if (#available(macOS 10.15, *)) {
CGDisplayStreamRef stream = CGDisplayStreamCreate(CGMainDisplayID(), 1, 1, kCVPixelFormatType_32BGRA, nil, ^(CGDisplayStreamFrameStatus status, uint64_t displayTime, IOSurfaceRef frameSurface, CGDisplayStreamUpdateRef updateRef) {
;
});
BOOL canRecord = stream != NULL;
if (stream) {
CFRelease(stream);
}
return canRecord;
} else {
return YES;
}
}
All of the solutions presented here have a flaw in one way or another. The root of the problem is that there's no correlation between your permission to know about a window (via the name in the window list), your permission to know about the process owner of the window (such as WindowServer and Dock). Your permission to view the pixels on screen is a combination of two sparse sets of information.
Here is a heuristic that covers all the cases as of macOS 10.15.1:
BOOL canRecordScreen = YES;
if (#available(macOS 10.15, *)) {
canRecordScreen = NO;
NSRunningApplication *runningApplication = NSRunningApplication.currentApplication;
NSNumber *ourProcessIdentifier = [NSNumber numberWithInteger:runningApplication.processIdentifier];
CFArrayRef windowList = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly, kCGNullWindowID);
NSUInteger numberOfWindows = CFArrayGetCount(windowList);
for (int index = 0; index < numberOfWindows; index++) {
// get information for each window
NSDictionary *windowInfo = (NSDictionary *)CFArrayGetValueAtIndex(windowList, index);
NSString *windowName = windowInfo[(id)kCGWindowName];
NSNumber *processIdentifier = windowInfo[(id)kCGWindowOwnerPID];
// don't check windows owned by this process
if (! [processIdentifier isEqual:ourProcessIdentifier]) {
// get process information for each window
pid_t pid = processIdentifier.intValue;
NSRunningApplication *windowRunningApplication = [NSRunningApplication runningApplicationWithProcessIdentifier:pid];
if (! windowRunningApplication) {
// ignore processes we don't have access to, such as WindowServer, which manages the windows named "Menubar" and "Backstop Menubar"
}
else {
NSString *windowExecutableName = windowRunningApplication.executableURL.lastPathComponent;
if (windowName) {
if ([windowExecutableName isEqual:#"Dock"]) {
// ignore the Dock, which provides the desktop picture
}
else {
canRecordScreen = YES;
break;
}
}
}
}
}
CFRelease(windowList);
}
If canRecordScreen is not set, you'll need to put up some kind of dialog that warns the user that they'll only be able to see the menubar, desktop picture, and the app's own windows. Here's how we presented it in our app xScope.
And yes, I'm still bitter that these protections were introduced with little regard to usability.
Apple provides direct low level api to check for access and grant access. No need to use tricky workarounds.
/* Checks whether the current process already has screen capture access */
#available(macOS 10.15, *)
public func CGPreflightScreenCaptureAccess() -> Bool
Use the above functions to check for screen capture access.
if access is not given use the below function to prompt for access
/* Requests event listening access if absent, potentially prompting */
#available(macOS 10.15, *)
public func CGRequestScreenCaptureAccess() -> Bool
Screenshot taken from documentation
#marek-h posted a good example that can detect the screen recording setting without showing privacy alert.
Btw, #jordan-h mentioned that this solution doesn't work when the app presents an alert via beginSheetModalForWindow.
I found that SystemUIServer process is always creating some windows with names: AppleVolumeExtra, AppleClockExtra, AppleBluetoothExtra ...
We can't get the names of these windows, before the screen recording is enabled in Privacy preferences. And when we can get one of these names at least, then it means that the user has enabled screen recording.
So we can check the names of the windows (created by SystemUIServer process) to detect the screen recording preference, and it works fine on macOS Catalina.
#include <AppKit/AppKit.h>
#include <libproc.h>
bool isScreenRecordingEnabled()
{
if (#available(macos 10.15, *)) {
bool bRet = false;
CFArrayRef list = CGWindowListCopyWindowInfo(kCGWindowListOptionAll, kCGNullWindowID);
if (list) {
int n = (int)(CFArrayGetCount(list));
for (int i = 0; i < n; i++) {
NSDictionary* info = (NSDictionary*)(CFArrayGetValueAtIndex(list, (CFIndex)i));
NSString* name = info[(id)kCGWindowName];
NSNumber* pid = info[(id)kCGWindowOwnerPID];
if (pid != nil && name != nil) {
int nPid = [pid intValue];
char path[PROC_PIDPATHINFO_MAXSIZE+1];
int lenPath = proc_pidpath(nPid, path, PROC_PIDPATHINFO_MAXSIZE);
if (lenPath > 0) {
path[lenPath] = 0;
if (strcmp(path, "/System/Library/CoreServices/SystemUIServer.app/Contents/MacOS/SystemUIServer") == 0) {
bRet = true;
break;
}
}
}
}
CFRelease(list);
}
return bRet;
} else {
return true;
}
}
I'm not aware of an API that's specifically for getting the screen recording permission status. Besides creating a CGDisplayStream and checking for nil, the Advances in macOS Security WWDC presentation also mentioned that certain metadata from the CGWindowListCopyWindowInfo() API will not be returned unless permission is granted. So something like this does seem to work, although it has the same issue of relying on implementation details of that function:
private func canRecordScreen() -> Bool {
guard let windows = CGWindowListCopyWindowInfo([.optionOnScreenOnly], kCGNullWindowID) as? [[String: AnyObject]] else { return false }
return windows.allSatisfy({ window in
let windowName = window[kCGWindowName as String] as? String
return windowName != nil
})
}
As of Nov19 chockenberry has correct answer.
As #onelittlefish pointed out the kCGWindowName is being omitted in case user has not enabled the screen recording access in privacy pane. This method also doesn't trigger the privacy alert.
- (BOOL)canRecordScreen
{
if (#available(macOS 10.15, *)) {
CFArrayRef windowList = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly, kCGNullWindowID);
NSUInteger numberOfWindows = CFArrayGetCount(windowList);
NSUInteger numberOfWindowsWithName = 0;
for (int idx = 0; idx < numberOfWindows; idx++) {
NSDictionary *windowInfo = (NSDictionary *)CFArrayGetValueAtIndex(windowList, idx);
NSString *windowName = windowInfo[(id)kCGWindowName];
if (windowName) {
numberOfWindowsWithName++;
} else {
//no kCGWindowName detected -> not enabled
break; //breaking early, numberOfWindowsWithName not increased
}
}
CFRelease(windowList);
return numberOfWindows == numberOfWindowsWithName;
}
return YES;
}
The most favorable answer is not exactly right, he left out some sences, like sharing state.
we can find the answer in WWDC(https://developer.apple.com/videos/play/wwdc2019/701/?time=1007)
Here are some excerpts from WWDC:
the window name and sharing state are not available, unless the user has preapproved the app for screen recording. And this is because some apps put sensitive data such as account names or more likely web page URLs in the window's name.
- (BOOL)ScreeningRecordPermissionCheck {
if (#available(macOS 10.15, *)) {
CFArrayRef windowList = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly, kCGNullWindowID);
NSUInteger numberOfWindows = CFArrayGetCount(windowList);
NSUInteger numberOfWindowsWithInfoGet = 0;
for (int idx = 0; idx < numberOfWindows; idx++) {
NSDictionary *windowInfo = (NSDictionary *)CFArrayGetValueAtIndex(windowList, idx);
NSString *windowName = windowInfo[(id)kCGWindowName];
NSNumber* sharingType = windowInfo[(id)kCGWindowSharingState];
if (windowName || kCGWindowSharingNone != sharingType.intValue) {
numberOfWindowsWithInfoGet++;
} else {
NSNumber* pid = windowInfo[(id)kCGWindowOwnerPID];
NSString* appName = windowInfo[(id)kCGWindowOwnerName];
NSLog(#"windowInfo get Fail pid:%lu appName:%#", pid.integerValue, appName);
}
}
CFRelease(windowList);
if (numberOfWindows == numberOfWindowsWithInfoGet) {
return YES;
} else {
return NO;
}
}
return YES;
}
As of MacOS 10.15.7 the heuristics of obtaining window-names for visible windows, and so know we have screen-capture permission, doesn't always work. Sometimes we just don't find valid windows we can query, and would wrongly deduce we don't have permissions.
However, I found another way to directly query (using sqlite) the Apple TCC database - the model where permissions are persisted. The screen-recording permissions are to be found in the "System level" TCC database ( residing in /Library/Application Support/com.apple.TCC/TCC.db). If you open the database using sqlite, and query: SELECT allowed FROM access WHERE client="com.myCompany.myApp" AND service="kTCCServiceScreenCapture" you'll get your answer.
Two downsides comparing to other answers:
to open this TCC.db database, your app must have "Full Disk Access" permission. It doesn't need to run with 'root' privileges, and root privileges won't help if you don't have the "Full disk access".
it takes about 15 millisec to run, which is slower than querying the window list.
The up side -- it's a direct query of the actual thing, and does not rely on any windows, or processes to exist at the time of query.
Here's some draft code to do this:
NSString *client = #"com.myCompany.myApp";
sqlite3 *tccDb = NULL;
sqlite3_stmt *statement = NULL;
NSString *pathToSystemTCCDB = #"/Library/Application Support/com.apple.TCC/TCC.db";
const char *pathToDBFile = [pathToSystemTCCDB fileSystemRepresentation];
if (sqlite3_open(pathToDBFile, &tccDb) != SQLITE_OK)
return nil;
const char *query = [[NSString stringWithFormat: #"SELECT allowed FROM access WHERE client=\"%#\" AND service=\"kTCCServiceScreenCapture\"",client] UTF8String];
if (sqlite3_prepare_v2(tccDb, query , -1, &statement, nil) != SQLITE_OK)
return nil;
BOOL allowed = NO;
while (sqlite3_step(statement) == SQLITE_ROW)
allowed |= (sqlite3_column_int(statement, 0) == 1);
if (statement)
sqlite3_finalize(statement);
if (tccDb)
sqlite3_close(tccDb);
return #(allowed);
}
Working for me.
Code from: https://gist.github.com/code4you2021/270859c71f90720d880ccb2474f4e7df
import Cocoa
struct ScreenRecordPermission {
static var hasPermission: Bool {
permissionCheck()
}
static func permissionCheck() -> Bool {
if #available(macOS 10.15, *) {
let runningApplication = NSRunningApplication.current
let processIdentifier = runningApplication.processIdentifier
guard let windows = CGWindowListCopyWindowInfo([.optionOnScreenOnly], kCGNullWindowID)
as? [[String: AnyObject]],
let _ = windows.first(where: { window -> Bool in
guard let windowProcessIdentifier = (window[kCGWindowOwnerPID as String] as? Int).flatMap(pid_t.init),
windowProcessIdentifier != processIdentifier,
let windowRunningApplication = NSRunningApplication(processIdentifier: windowProcessIdentifier),
windowRunningApplication.executableURL?.lastPathComponent != "Dock",
let _ = window[String(kCGWindowName)] as? String
else {
return false
}
return true
})
else {
return false
}
}
return true
}
static func requestPermission() {
if #available(macOS 10.15, *) {
CGWindowListCreateImage(CGRect(x: 0, y: 0, width: 1, height: 1), .optionOnScreenOnly, kCGNullWindowID, [])
}
}
}
# how to use
# print("hasPermission: ", ScreenRecordPermission.hasPermission)
The above answer is not working fine. Below is the correct answer.
private var canRecordScreen : Bool {
guard let windows = CGWindowListCopyWindowInfo([.optionOnScreenOnly], kCGNullWindowID) as? [[String: AnyObject]] else { return false }
return windows.allSatisfy({ window in
let windowName = window[kCGWindowName as String] as? String
let isSharingEnabled = window[kCGWindowSharingState as String] as? Int
return windowName != nil || isSharingEnabled == 1
})
}
Related
With file access in a sandboxed osx app with swift in mind, does it work the same with URLs provided via Finder or other apps drops?
As there's no NSOpenPanel call to afford folder access as in this example, just urls - I think the folder access is implicit since the user dragged the file from the source / desktop "folder" much the same as implicit selection via the open dialog.
I have not begun the sandbox migration yet but wanted to verify my thinking was accurate, but here's a candidate routine that does not work in sandbox mode:
func performDragOperation(_ sender: NSDraggingInfo!) -> Bool {
let pboard = sender.draggingPasteboard()
let items = pboard.pasteboardItems
if (pboard.types?.contains(NSURLPboardType))! {
for item in items! {
if let urlString = item.string(forType: kUTTypeURL as String) {
self.webViewController.loadURL(text: urlString)
}
else
if let urlString = item.string(forType: kUTTypeFileURL as String/*"public.file-url"*/) {
let fileURL = NSURL.init(string: urlString)?.filePathURL
self.webViewController.loadURL(url: fileURL!)
}
else
{
Swift.print("items has \(item.types)")
}
}
}
else
if (pboard.types?.contains(NSPasteboardURLReadingFileURLsOnlyKey))! {
Swift.print("we have NSPasteboardURLReadingFileURLsOnlyKey")
}
return true
}
as no URL is acted upon or error thrown.
Yes, the file access is implicit. As the sandbox implementation is poorly documented and had/has many bugs, you want to work around URL and Filenames. The view should register itself for both types at initialisation. Code is in Objective-C, but API should be the same.
[self registerForDraggedTypes:[NSArray arrayWithObjects:NSFilenamesPboardType, NSURLPboardType, nil]];
Then on performDragOperation:
- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender
{
BOOL dragPerformed = NO;
NSPasteboard *paste = [sender draggingPasteboard];
NSArray *typesWeRead = [NSArray arrayWithObjects:NSFilenamesPboardType, NSURLPboardType, nil];
//a list of types that we can accept
NSString *typeInPasteboard = [paste availableTypeFromArray:typesWeRead];
if ([typeInPasteboard isEqualToString:NSFilenamesPboardType]) {
NSArray *fileArray = [paste propertyListForType:#"NSFilenamesPboardType"];
//be careful since this method returns id.
//We just happen to know that it will be an array. and it contains strings.
NSMutableArray *urlArray = [NSMutableArray arrayWithCapacity:[fileArray count]];
for (NSString *path in fileArray) {
[urlArray addObject:[NSURL fileURLWithPath:path]];
}
dragPerformed = //.... do your stuff with the files;
} else if ([typeInPasteboard isEqualToString:NSURLPboardType]) {
NSURL *droppedURL = [NSURL URLFromPasteboard:paste];
if ([droppedURL isFileURL]) {
dragPerformed = //.... do your stuff with the files;
}
}
return dragPerformed;
}
I'm wondering if it's possible to get any tab/window info from safari programmatically?
Is there a library to do it?
I'd prefer not applescript, as I've found that - I'd like to know if, and how it's possible in the Cocoa framework.
You can do this with Scripting Bridge, which is like AppleScript translated into Objective-C, or with accessibility objects, which you can inspect with Developer Tool Accessibility Inspector. Both technogies have their quirks and aren't documented very well.
Edit:
Scripting Bridge example:
SafariApplication *SafariApp = [SBApplication applicationWithBundleIdentifier:#"com.apple.Safari"];
for (SafariWindow *window in SafariApp.windows)
{
for (SafariTab *tab in window.tabs)
NSLog(#"%#", tab.name);
}
The hierarchy of accessibility objects in Safari is
AXApplication
AXWindow
AXTabGroup
AXRadioButton
Example (doesn't win a prize in a beauty contest):
static NSArray *getAXUIElements(AXUIElementRef theContainer, CFStringRef theRole)
{
// get children of theContainer
AXError error;
NSMutableArray *array = [NSMutableArray array];
CFTypeRef children;
error = AXUIElementCopyAttributeValue(theContainer, kAXChildrenAttribute, &children);
if (error != kAXErrorSuccess)
return nil;
// filter children whose role is theRole
for (CFIndex i = 0; i < CFArrayGetCount(children); i++)
{
AXUIElementRef child = CFArrayGetValueAtIndex(children, i);
CFTypeRef role;
error = AXUIElementCopyAttributeValue(child, kAXRoleAttribute, &role);
if (error == kAXErrorSuccess)
{
if (CFStringCompare(role, theRole, 0) == kCFCompareEqualTo)
[array addObject:(__bridge id)child];
CFRelease(role);
}
}
CFRelease(children);
return [NSArray arrayWithArray:array];
}
static void logTabs()
{
// get the title of every tab of every window of Safari
NSArray *appArray = [NSRunningApplication runningApplicationsWithBundleIdentifier:#"com.apple.Safari"];
AXUIElementRef SafariApp = AXUIElementCreateApplication([[appArray objectAtIndex:0] processIdentifier]);
if (SafariApp)
{
NSArray *windowArray = getAXUIElements(SafariApp, kAXWindowRole);
for (id window in windowArray)
{
NSArray *tabGroupArray = getAXUIElements((__bridge AXUIElementRef)(window), kAXTabGroupRole);
for (id tabGroup in tabGroupArray)
{
NSArray *radioButtonArray = getAXUIElements((__bridge AXUIElementRef)(tabGroup), kAXRadioButtonRole);
for (id radioButton in radioButtonArray)
{
CFTypeRef title = NULL;
AXError error = AXUIElementCopyAttributeValue((__bridge AXUIElementRef)radioButton, kAXTitleAttribute, &title);
if (error == kAXErrorSuccess)
{
NSLog(#"%#", title);
CFRelease(title);
}
}
}
}
CFRelease(SafariApp);
}
}
I have developed a Mac app which I want to start as soon as the Mac is started. How can I achieve this functionality programmatically?
I know about launchd, but can't find a working example.
This code below was developed from an excellent blog post by Tim Schroeder: The Launch At Login Sandbox Project. Actually more or less borrowed completely from there-- it looks like I only ever had to change the log messages and strings in there, so it's pretty rock solid if I never screwed with it.
Where my opinion differs with that post is that there should be no "helper" app, even if you want the app to launch without a GUI at login. It's unnecessary. If you want your app to have a background mode that runs at login or when the use closes the GUI, you should switch the app to accessory mode as detailed in my answer here. You don't need to deal with subprojects and compiling separate executables. Just have one.
Anyways, Tim's eminently useful code. It uses a segmented control on a preferences panel to toggle the app on/off. The segmentedControl value uses cocoa-bindings to bind the control value to NSUserDefaults, so all that's seen here is registering/unregistering the app as a login item, and error checking/alerting.
- (IBAction)toggleRunAtLogin:(NSSegmentedControl*)sender {
NSLog(#"toggling run at login");
NSUInteger clickedSegment = [sender selectedSegment];
if (clickedSegment == 0) { // ON
// Turn on launch at login
NSLog(#"... to ON");
if (!SMLoginItemSetEnabled ((__bridge CFStringRef)#"com.yourCo.yourApp", YES)) {
NSAlert *alert = [NSAlert alertWithMessageText:#"An error ocurred"
defaultButton:#"OK"
alternateButton:nil
otherButton:nil
informativeTextWithFormat:#"Couldn't add App to launch at login item list."];
[alert runModal];
}
}
if (clickedSegment == 1) { // OFF
// Turn off launch at login
NSLog(#"... to OFF");
if (!SMLoginItemSetEnabled ((__bridge CFStringRef)#"com.yourCo.yourApp", NO)) {
NSAlert *alert = [NSAlert alertWithMessageText:#"An error ocurred"
defaultButton:#"OK"
alternateButton:nil
otherButton:nil
informativeTextWithFormat:#"Couldn't remove App from launch at login item list."];
[alert runModal];
}
}
}
A great example of the implementation of this can be found at https://github.com/nfarina/feeds/blob/master/Feeds/LoginItems.m
#import "LoginItems.h"
#implementation LoginItems
// Copied from https://github.com/carpeaqua/Shared-File-List-Example
- (void)enableLoginItemWithLoginItemsReference:(LSSharedFileListRef )theLoginItemsRefs ForPath:(NSString *)appPath {
// We call LSSharedFileListInsertItemURL to insert the item at the bottom of Login Items list.
CFURLRef url = (CFURLRef)[NSURL fileURLWithPath:appPath];
LSSharedFileListItemRef item = LSSharedFileListInsertItemURL(theLoginItemsRefs, kLSSharedFileListItemLast, NULL, NULL, url, NULL, NULL);
if (item)
CFRelease(item);
}
- (void)disableLoginItemWithLoginItemsReference:(LSSharedFileListRef )theLoginItemsRefs ForPath:(NSString *)appPath {
UInt32 seedValue;
CFURLRef thePath = NULL;
// We're going to grab the contents of the shared file list (LSSharedFileListItemRef objects)
// and pop it in an array so we can iterate through it to find our item.
CFArrayRef loginItemsArray = LSSharedFileListCopySnapshot(theLoginItemsRefs, &seedValue);
for (id item in (NSArray *)loginItemsArray) {
LSSharedFileListItemRef itemRef = (LSSharedFileListItemRef)item;
if (LSSharedFileListItemResolve(itemRef, 0, (CFURLRef*) &thePath, NULL) == noErr) {
if ([[(NSURL *)thePath path] hasPrefix:appPath]) {
LSSharedFileListItemRemove(theLoginItemsRefs, itemRef); // Deleting the item
}
// Docs for LSSharedFileListItemResolve say we're responsible
// for releasing the CFURLRef that is returned
if (thePath != NULL) CFRelease(thePath);
}
}
if (loginItemsArray != NULL) CFRelease(loginItemsArray);
}
- (BOOL)loginItemExistsWithLoginItemReference:(LSSharedFileListRef)theLoginItemsRefs ForPath:(NSString *)appPath {
BOOL found = NO;
UInt32 seedValue;
CFURLRef thePath = NULL;
// We're going to grab the contents of the shared file list (LSSharedFileListItemRef objects)
// and pop it in an array so we can iterate through it to find our item.
CFArrayRef loginItemsArray = LSSharedFileListCopySnapshot(theLoginItemsRefs, &seedValue);
for (id item in (NSArray *)loginItemsArray) {
LSSharedFileListItemRef itemRef = (LSSharedFileListItemRef)item;
if (LSSharedFileListItemResolve(itemRef, 0, (CFURLRef*) &thePath, NULL) == noErr) {
if ([[(NSURL *)thePath path] hasPrefix:appPath]) {
found = YES;
break;
}
// Docs for LSSharedFileListItemResolve say we're responsible
// for releasing the CFURLRef that is returned
if (thePath != NULL) CFRelease(thePath);
}
}
if (loginItemsArray != NULL) CFRelease(loginItemsArray);
return found;
}
// Our code
+ (LoginItems *)userLoginItems {
static LoginItems *userItems = nil;
return userItems ?: (userItems = [LoginItems new]);
}
- (BOOL)currentAppLaunchesAtStartup {
NSString * appPath = [[NSBundle mainBundle] bundlePath];
LSSharedFileListRef loginItems = LSSharedFileListCreate(NULL, kLSSharedFileListSessionLoginItems, NULL);
BOOL exists = [self loginItemExistsWithLoginItemReference:loginItems ForPath:appPath];
CFRelease(loginItems);
return exists;
}
- (void)setCurrentAppLaunchesAtStartup:(BOOL)currentAppLaunchesAtStartup {
NSString * appPath = [[NSBundle mainBundle] bundlePath];
LSSharedFileListRef loginItems = LSSharedFileListCreate(NULL, kLSSharedFileListSessionLoginItems, NULL);
if (loginItems) {
if (!self.currentAppLaunchesAtStartup)
[self enableLoginItemWithLoginItemsReference:loginItems ForPath:appPath];
else
[self disableLoginItemWithLoginItemsReference:loginItems ForPath:appPath];
CFRelease(loginItems);
}
}
#end
I found the solution
Thanks
- (void)enableLoginItemWithLoginItemsReference:(LSSharedFileListRef)theLoginItemsRefs ForPath:(NSString *)appPath {
// We call LSSharedFileListInsertItemURL to insert the item at the bottom of Login Items list.
CFURLRef url = (__bridge CFURLRef)[NSURL fileURLWithPath:appPath];
LSSharedFileListItemRef item = LSSharedFileListInsertItemURL(theLoginItemsRefs, kLSSharedFileListItemLast, NULL, NULL, url, NULL, NULL);
if (item)
CFRelease(item);
}
- (void)disableLoginItemWithLoginItems {
UInt32 seedValue;
CFURLRef thePath = NULL;
NSString * appPath = [[NSBundle mainBundle] bundlePath];
// Create a reference to the shared file list.
LSSharedFileListRef loginItems = LSSharedFileListCreate(NULL, kLSSharedFileListSessionLoginItems, NULL);
// We're going to grab the contents of the shared file list (LSSharedFileListItemRef objects)
// and pop it in an array so we can iterate through it to find our item.
CFArrayRef loginItemsArray = LSSharedFileListCopySnapshot(loginItems, &seedValue);
for (id item in (__bridge NSArray *)loginItemsArray) {
LSSharedFileListItemRef itemRef = (__bridge LSSharedFileListItemRef)item;
if (LSSharedFileListItemResolve(itemRef, 0, (CFURLRef*) &thePath, NULL) == noErr) {
if ([[(__bridge NSURL *)thePath path] hasPrefix:appPath]) {
LSSharedFileListItemRemove(loginItems, itemRef); // Deleting the item
}
// Docs for LSSharedFileListItemResolve say we're responsible
// for releasing the CFURLRef that is returned
if (thePath != NULL) CFRelease(thePath);
}
}
if (loginItemsArray != NULL) CFRelease(loginItems);
}
- (BOOL)loginItemExistsWithLoginItemReference:(LSSharedFileListRef)theLoginItemsRefs ForPath:(NSString *)appPath {
BOOL found = NO;
UInt32 seedValue;
CFURLRef thePath = NULL;
// We're going to grab the contents of the shared file list (LSSharedFileListItemRef objects)
// and pop it in an array so we can iterate through it to find our item.
CFArrayRef loginItemsArray = LSSharedFileListCopySnapshot(theLoginItemsRefs, &seedValue);
for (id item in (__bridge NSArray *)loginItemsArray) {
LSSharedFileListItemRef itemRef = (__bridge LSSharedFileListItemRef)item;
if (LSSharedFileListItemResolve(itemRef, 0, (CFURLRef*) &thePath, NULL) == noErr) {
if ([[(__bridge NSURL *)thePath path] hasPrefix:appPath]) {
found = YES;
break;
}
// Docs for LSSharedFileListItemResolve say we're responsible
// for releasing the CFURLRef that is returned
if (thePath != NULL) CFRelease(thePath);
}
}
if (loginItemsArray != NULL) CFRelease(loginItemsArray);
return found;
}
Sample code is
[[[NSWorkspace sharedWorkspace] notificationCenter] addObserver:self selector:#selector(activeSpaceDidChange:) name:NSWorkspaceActiveSpaceDidChangeNotification object:nil];
Then
- (void) activeSpaceDidChange:(NSNotification *)aNotification {
// code to check if current workspace is dashboard?
}
I want to check whether the current space is dashboard or not? Any idea?
The first think i have tried is get to the current space id according to this answer: Detecting when a space changes in Spaces in Mac OS X . The problem here is that the key kCGWindowWorkspace is deprecated in OSX 10.8. So there is no direct way to get this information.
In my solution now i check for different windows or owners which are only one the dashboard space or on all other spaces:
The user is on the dashboard if there is one window which kCGWindowName ends with .wdgt/
The user is not on the dashboard if there is one window with kCGWindowName == System Status Item Clone, kCGWindowOwnerName == SystemUIServer | Finder
So why i'm not just using the .wdgt/ check? -- Because if there is now widget on the dashboard this not working
So why i'm using more than one window check? -. Because i'm not jet sure which window is always on all spaces. At least System Status Item Clone and Finder are not always there.
Here my implementation is add this function as category to NSWorkspace
- (BOOL) userIsOnDashboardSpace {
NSArray* windowsInSpace = (__bridge NSArray *) CGWindowListCopyWindowInfo(kCGWindowListOptionAll | kCGWindowListOptionOnScreenOnly, kCGNullWindowID);
NSUInteger indexOfWidget = [windowsInSpace indexOfObjectPassingTest:^BOOL(NSDictionary* obj, NSUInteger idx, BOOL *stop) {
if ([obj objectForKey:(id)kCGWindowName]) {
NSString *name = (NSString *)[obj objectForKey:(id)kCGWindowName];
if ([name isEqualToString:#"System Status Item Clone"]) {
*stop = true;
return false;
}
if ([name hasSuffix:#".wdgt/"]) {
*stop = true;
return true;
}
}
if ([obj objectForKey:(id)kCGWindowOwnerName]) {
NSString *name = (NSString *)[obj objectForKey:(id)kCGWindowOwnerName];
if ([name isEqualToString:#"SystemUIServer"]) {
*stop = true;
return false;
}
if ([name isEqualToString:#"Finder"]) {
*stop = true;
return false;
}
}
return false;
}];
return indexOfWidget != NSNotFound;
}
I want to badge a file and folder with some color (image). How can this be achieved?
I tried with icon service, and it works for files, but it is not working with folders.
I saw this behavior working Dropbox (10.4, 10.5 and 10.6)- how can this be done?
The blog post Cocoa Tutorial: Custom Folder Icons was very close one for me, but it was not working as expected.
Is there another solution other than icon service?
The following function is the solution I found for the problem
BOOL AddBadgeToItem(NSString* path,NSData* tag)
{
FSCatalogInfo info;
FSRef par;
FSRef ref;
Boolean dir = false;
if (tag&&(FSPathMakeRef([path fileSystemRepresentation],&par,&dir)==noErr))
{
HFSUniStr255 fork = {0,{0}};
sint16 refnum = kResFileNotOpened;
FSGetResourceForkName(&fork);
if (dir)
{
NSString *name = #"Icon\r";
memset(&info,0,sizeof(info));
((FileInfo*)(&info.finderInfo))->finderFlags = kIsInvisible;
OSErr error = FSCreateResourceFile(&par,[name lengthOfBytesUsingEncoding:NSUTF16LittleEndianStringEncoding],(UniChar*)[name cStringUsingEncoding:NSUTF16LittleEndianStringEncoding],kFSCatInfoFinderXInfo,&info,fork.length, fork.unicode,&ref,NULL);
if( error == dupFNErr )
{
// file already exists; prepare to try to open it
const char *iconFileSystemPath = [[path stringByAppendingPathComponent:#"\000I\000c\000o\000n\000\r"] fileSystemRepresentation];
OSStatus status = FSPathMakeRef((const UInt8 *)iconFileSystemPath, &ref, NULL);
if (status != noErr)
{
fprintf(stderr, "error: FSPathMakeRef() returned %d for file \"%s\"\n", (int)status, iconFileSystemPath);
}
}else if ( error != noErr)
{
return NO;
}
}
else
{
BlockMoveData(&par,&ref,sizeof(FSRef));
if (FSCreateResourceFork(&ref,fork.length,fork.unicode,0)!=noErr)
{
//test
if (FSOpenResourceFile(&ref,fork.length,fork.unicode,fsRdWrPerm,&refnum)!=noErr) {
return NO;
}
if (refnum!=kResFileNotOpened) {
UpdateResFile(refnum);
CloseResFile(refnum);
if (FSGetCatalogInfo(&par,kFSCatInfoFinderXInfo,&info,NULL,NULL,NULL)==noErr) {
((ExtendedFileInfo*)(&info.extFinderInfo))->extendedFinderFlags = kExtendedFlagsAreInvalid;
FSSetCatalogInfo(&par,kFSCatInfoFinderXInfo,&info);
}
}
//Test end
return NO;
}
}
OSErr errorr = FSOpenResourceFile(&ref,fork.length,fork.unicode,fsRdWrPerm,&refnum);
if (errorr!=noErr) {
return NO;
}
if (refnum!=kResFileNotOpened) {
CustomBadgeResource* cbr;
int len = [tag length];
Handle h = NewHandle(len);
if (h) {
BlockMoveData([tag bytes],*h,len);
AddResource(h,kIconFamilyType,128,"\p");
WriteResource(h);
ReleaseResource(h);
}
h = NewHandle(sizeof(CustomBadgeResource));
if (h) {
cbr = (CustomBadgeResource*)*h;
memset(cbr,0,sizeof(CustomBadgeResource));
cbr->version = kCustomBadgeResourceVersion;
cbr->customBadgeResourceID = 128;
AddResource(h,kCustomBadgeResourceType,kCustomBadgeResourceID,"\p");
WriteResource(h);
ReleaseResource(h);
}
UpdateResFile(refnum);
CloseResFile(refnum);
if (FSGetCatalogInfo(&par,kFSCatInfoFinderXInfo,&info,NULL,NULL,NULL)==noErr) {
((ExtendedFileInfo*)(&info.extFinderInfo))->extendedFinderFlags = kExtendedFlagHasCustomBadge;
FSSetCatalogInfo(&par,kFSCatInfoFinderXInfo,&info);
}
}
}
return NO;
}
You can do this using the -setIcon:forFile:options: method on NSWorkspace, which lets you just specify an NSImage to apply to the file/folder at the path you give it.
The intended approach is to create a Finder Sync app extension.