I already know how to use the Mac OSX Accessibility API within Objective-C to reposition windows of another running application, without the use of any kind of scripting bridge.
Now, I want to use this same Accessibility API (again, without any scripting bridge) to close all the open windows of another running application.
The code that I want to write in Objective-C should do the same thing as this AppleScript code:
tell application "TheApplication"
close every window
end tell
I would guess that this is possible, because it's permitted within AppleScript.
Here's my solution ...
+(void)closeWindowsOfApp:(NSString*)appName {
boolean_t result = false;
if (appName == nil) {
return;
}
ProcessSerialNumber psn;
psn.highLongOfPSN = 0;
psn.lowLongOfPSN = kNoProcess;
while (GetNextProcess(&psn) == noErr) {
pid_t pid = 0;
if (GetProcessPID(&psn, &pid) != noErr) {
continue;
}
AXUIElementRef elementRef = AXUIElementCreateApplication(pid);
NSString* title = nil;
AXUIElementCopyAttributeValue(elementRef, kAXTitleAttribute, (CFTypeRef *)&title);
if (title == nil) {
continue;
}
if ([title compare:appName] != NSOrderedSame) {
CFRelease(title);
continue;
}
CFRelease(title);
CFArrayRef windowArray = nil;
AXUIElementCopyAttributeValue(elementRef, kAXWindowsAttribute, (CFTypeRef*)&windowArray);
if (windowArray == nil) {
CFRelease(elementRef);
continue;
}
CFRelease(elementRef);
CFIndex nItems = CFArrayGetCount(windowArray);
if (nItems < 1) {
CFRelease(windowArray);
continue;
}
for (int i = 0; i < nItems; i++) {
AXUIElementRef itemRef = (AXUIElementRef) CFArrayGetValueAtIndex(windowArray, i);
AXUIElementRef buttonRef = nil;
AXUIElementCopyAttributeValue(itemRef, kAXCloseButtonAttribute, (CFTypeRef*)&buttonRef);
AXUIElementPerformAction(buttonRef, kAXPressAction);
CFRelease(buttonRef);
}
CFRelease(windowArray);
break;
}
}
There's a Cocoa class, NSApplescript, that lets you compile and run Applescript from within your ObjC code. You haven't really said why you don't want to use AS. Since you've already got the script that does what you want, you can make your program work right now and just use it:
NSApplescript * as = [[NSApplescript alloc] initWithSource:#"tell application \"TheApplication\"\nclose every window\nend tell"];
NSDictionary * errInfo;
NSAppleEventDescriptor * res = [as executeAndReturnError:&err];
if( !res ){
// An error occurred. Inspect errInfo and perform necessary actions
}
[as release];
Worry about ideological purity or performance later.
Related
I want to enable dictation on any keyboard key press.
Currently I am using AppleScript to click on Menu->Edit->Start Dictation of front most app.
This script requires Automation permissions and I am hesitating to create app with so many permissions.
Is there a way to get the shortcut keys assigned to Dictation feature?
example some preference file in ~/Library/Preferences...
Goal is to:
Read shortcut keys from preference file
Fire those keys from app (CGEVENTPOST)
I ended up implementing myself. Captured changes made to Preferences folder after modifying any Keyboard setting.
Came across a file called com.apple.symbolichotkeys.plist which changed when any shortcuts were modified.
Here is sample code implemented to enable dictation programmatically (Hope my code is self explanatory)
#include <Carbon/Carbon.h>
[self enableDication];
-(void) enableDication
{
int press_count = 2;
NSString *pathSymbHotKeys = #"~/Library/Preferences/com.apple.symbolichotkeys.plist";
NSMutableDictionary *symbolicHotKeysDict = [[NSMutableDictionary alloc]initWithContentsOfFile:pathSymbHotKeys.stringByExpandingTildeInPath];
NSNumber *enabled = symbolicHotKeysDict[#"AppleSymbolicHotKeys"][#"164"][#"enabled"] ;
if (![enabled boolValue]) {
NSLog(#"Dication shortcut not enabled");
return;
}
NSString *type = symbolicHotKeysDict[#"AppleSymbolicHotKeys"][#"164"][#"value"][#"type"];
NSNumber *param_key_code = nil;
NSNumber *param_mask = nil;
if ([type isEqualToString:#"standard"]) {
//mask
param_mask = symbolicHotKeysDict[#"AppleSymbolicHotKeys"][#"164"][#"value"][#"parameters"][2];
param_key_code = symbolicHotKeysDict[#"AppleSymbolicHotKeys"][#"164"][#"value"][#"parameters"][1];
press_count = 1;
}
else {
param_key_code = symbolicHotKeysDict[#"AppleSymbolicHotKeys"][#"164"][#"value"][#"parameters"][0];
}
CGEventRef fnDown = CGEventCreateKeyboardEvent(NULL, [self keyCode:param_key_code type:type], true);
if ([type isEqualToString:#"standard"]) {
CGEventSetFlags(fnDown, (CGEventFlags)([param_mask intValue] | CGEventGetFlags(fnDown)));
}
CGEventRef fnUp = CGEventCreateKeyboardEvent(NULL, [self keyCode:param_key_code type:type], false);
if ([type isEqualToString:#"standard"]) {
CGEventSetFlags(fnUp, (CGEventFlags)([param_mask intValue]));
}
for (int i = 0; i <press_count; i++)
{
CGEventPost(kCGSessionEventTap, fnDown);
CGEventPost(kCGSessionEventTap, fnUp);
}
CFRelease (fnDown);
CFRelease (fnUp);
}
-(CGKeyCode) keyCode:(NSNumber *)code type:(NSString *)type
{
if ([type isEqualToString:#"standard"]) {
return (CGKeyCode)[code longValue];
}
else{
//Default Modifier shortcuts provided by Mac for dictation
if (([code longValue] & 0x00100008) == 0x00100008) {
//Left command key
return kVK_Command;
}
else if (([code longValue] & 0x00100010) == 0x00100010) {
// right command key
return kVK_RightCommand;
}
else if (([code longValue] & kCGEventFlagMaskCommand) == kCGEventFlagMaskCommand) {
//any command key
return kVK_Command;
}
else if (([code longValue] & NSEventModifierFlagFunction) == NSEventModifierFlagFunction)
{
//function key
return kVK_Function;
}
else if (([code longValue] & kCGEventFlagMaskControl) == kCGEventFlagMaskControl)
{
//control key
return kVK_Control;
}
}
return -1;
}
Is it possible to find out if a Mac is Force Touch capable - either via a built-in Trackpad, like the new MacBook, or a Bluetooth device like the Magic Trackpad 2?
I'd like to present preferences specific to Force Touch if the Mac is Force Touch capable, but not display (or disable) those preferences if Force Touch is not available.
In the portion after the separator, you see the options I have in mind in the pic linked here. (sorry, embedding the pic itself didn't work).
So, not showing the preferences wouldn't restrict users who don't have force touch, it would just let users who have it configure how it should work, and those settings would be useless to users who don't have it.
Is there a way to achieve this?
Thank you and kind regards,
Matt
Edit: It's in Objective-C.
I figured it out:
+ (BOOL)isForceTouchCapable
{
if (![[self class] isAtLeastElCapitan])
return NO;
io_iterator_t iterator;
//get default HIDDevice dictionary
CFMutableDictionaryRef mDict = IOServiceMatching(kIOHIDDeviceKey);
//add manufacturer "Apple Inc." to dict
CFDictionaryAddValue(mDict, CFSTR(kIOHIDManufacturerKey), CFSTR("Apple Inc."));
//get matching services, depending on dict
IOReturn ioReturnValue = IOServiceGetMatchingServices(kIOMasterPortDefault, mDict, &iterator);
BOOL result = YES;
if (ioReturnValue != kIOReturnSuccess)
NSLog(#"error getting matching services for force touch devices");
else
{
//recursively go through each device found and its children and grandchildren, etc.
result = [[self class] _containsForceTouchDevice:iterator];
IOObjectRelease(iterator);
}
return result;
}
+ (BOOL)_containsForceTouchDevice:(io_iterator_t)iterator
{
io_object_t object = 0;
BOOL success = NO;
while ((object = IOIteratorNext(iterator)))
{
CFMutableDictionaryRef result = NULL;
kern_return_t state = IORegistryEntryCreateCFProperties(object, &result, kCFAllocatorDefault, 0);
if (state == KERN_SUCCESS && result != NULL)
{
if (CFDictionaryContainsKey(result, CFSTR("DefaultMultitouchProperties")))
{
CFDictionaryRef dict = CFDictionaryGetValue(result, CFSTR("DefaultMultitouchProperties"));
CFTypeRef val = NULL;
if (CFDictionaryGetValueIfPresent(dict, CFSTR("ForceSupported"), &val))
{
Boolean aBool = CFBooleanGetValue(val);
if (aBool) //supported
success = YES;
}
}
}
if (result != NULL)
CFRelease(result);
if (success)
{
IOObjectRelease(object);
break;
} else
{
io_iterator_t childIterator = 0;
kern_return_t err = IORegistryEntryGetChildIterator(object, kIOServicePlane, &childIterator);
if (!err)
{
success = [[self class] _containsForceTouchDevice:childIterator];
IOObjectRelease(childIterator);
} else
success = NO;
IOObjectRelease(object);
}
}
return success;
}
Just call + (BOOL)isForceTouchCapable and it will return YES if a Force Touch device is available (a Magic Trackpad 2 or a built in force-touch-trackpad) or NO if there isn't.
For those interested in how this came to be, I wrote about it on my blog with an example project.
I did small app to allow quickly change screen resolutions on multiple monitors. I want to show product name as title of the monitor, and it's very simple to find using this code:
NSDictionary *deviceInfo = (__bridge NSDictionary *)IODisplayCreateInfoDictionary(CGDisplayIOServicePort(dispID), kIODisplayOnlyPreferredName);
NSDictionary *localizedNames = [deviceInfo objectForKey:[NSString stringWithUTF8String:kDisplayProductName]];
if([localizedNames count] > 0) {
_title = [localizedNames objectForKey:[[localizedNames allKeys] objectAtIndex:0]];
} else {
_title = #"Unknown display";
}
But CGDisplayIOServicePort is deprecated in OS X >= 10.9 and Apple's documentation says there is no replacement. How to find service port or product name without using this method?
I tried to iterate through IO-registry and tried to use IOServiceGetMatchingServices method to find display services but I'm not very familiar with IO-registry so I couldn't find solution.
Thanks for help!
It looks like #Eun's post missed a piece of information to close this discussion. With a little search, I found that IOServicePortFromCGDisplayID is not an API which Apple provides. Rather, it's a piece of open source code found here:
https://github.com/glfw/glfw/blob/e0a6772e5e4c672179fc69a90bcda3369792ed1f/src/cocoa_monitor.m
I copied IOServicePortFromCGDisplayID and also 'getDisplayName' from it.
I needed two tweaks to make it work on OS X 10.10.
Remove the code to handle serial number in IOServicePortFromCGDisplayID. (CFDictionaryGetValue for
kDisplaySerialNumber returns NULL for me.)
Remove project specific
error handling code in getDisplayName.
If you need more information
Issue tracker of the problem: github.com/glfw/glfw/issues/165
Commit
for the solution:
github.com/glfw/glfw/commit/e0a6772e5e4c672179fc69a90bcda3369792ed1f
I would thank Matthew Henry who submitted the code there.
Here is my take on the issue. I also started with the code from GLFW 3.1, file cocoa_monitor.m.
But I had to modify it in different ways than Hiroshi said, so here goes:
// Get the name of the specified display
- (NSString*) screenNameForDisplay: (NSNumber*) screen_id
{
CGDirectDisplayID displayID = [screen_id unsignedIntValue];
io_service_t serv = [self IOServicePortFromCGDisplayID: displayID];
if (serv == 0)
return #"unknown";
CFDictionaryRef info = IODisplayCreateInfoDictionary(serv, kIODisplayOnlyPreferredName);
IOObjectRelease(serv);
CFStringRef display_name;
CFDictionaryRef names = CFDictionaryGetValue(info, CFSTR(kDisplayProductName));
if ( !names ||
!CFDictionaryGetValueIfPresent(names, CFSTR("en_US"), (const void**) & display_name) )
{
// This may happen if a desktop Mac is running headless
CFRelease( info );
return #"unknown";
}
NSString * displayname = [NSString stringWithString: (__bridge NSString *) display_name];
CFRelease(info);
return displayname;
}
// Returns the io_service_t (an int) corresponding to a CG display ID, or 0 on failure.
// The io_service_t should be released with IOObjectRelease when not needed.
- (io_service_t) IOServicePortFromCGDisplayID: (CGDirectDisplayID) displayID
{
io_iterator_t iter;
io_service_t serv, servicePort = 0;
CFMutableDictionaryRef matching = IOServiceMatching("IODisplayConnect");
// releases matching for us
kern_return_t err = IOServiceGetMatchingServices( kIOMasterPortDefault, matching, & iter );
if ( err )
return 0;
while ( (serv = IOIteratorNext(iter)) != 0 )
{
CFDictionaryRef displayInfo;
CFNumberRef vendorIDRef;
CFNumberRef productIDRef;
CFNumberRef serialNumberRef;
displayInfo = IODisplayCreateInfoDictionary( serv, kIODisplayOnlyPreferredName );
Boolean success;
success = CFDictionaryGetValueIfPresent( displayInfo, CFSTR(kDisplayVendorID), (const void**) & vendorIDRef );
success &= CFDictionaryGetValueIfPresent( displayInfo, CFSTR(kDisplayProductID), (const void**) & productIDRef );
if ( !success )
{
CFRelease(displayInfo);
continue;
}
SInt32 vendorID;
CFNumberGetValue( vendorIDRef, kCFNumberSInt32Type, &vendorID );
SInt32 productID;
CFNumberGetValue( productIDRef, kCFNumberSInt32Type, &productID );
// If a serial number is found, use it.
// Otherwise serial number will be nil (= 0) which will match with the output of 'CGDisplaySerialNumber'
SInt32 serialNumber = 0;
if ( CFDictionaryGetValueIfPresent(displayInfo, CFSTR(kDisplaySerialNumber), (const void**) & serialNumberRef) )
{
CFNumberGetValue( serialNumberRef, kCFNumberSInt32Type, &serialNumber );
}
// If the vendor and product id along with the serial don't match
// then we are not looking at the correct monitor.
// NOTE: The serial number is important in cases where two monitors
// are the exact same.
if( CGDisplayVendorNumber(displayID) != vendorID ||
CGDisplayModelNumber(displayID) != productID ||
CGDisplaySerialNumber(displayID) != serialNumber )
{
CFRelease(displayInfo);
continue;
}
servicePort = serv;
CFRelease(displayInfo);
break;
}
IOObjectRelease(iter);
return servicePort;
}
This works fine for me in a screensaver I wrote under macOS 10.11 (El Capitan).
I tested it with the built-in display of my MacBookPro and an Apple Display connected via Thunderbolt.
As of macOS 10.15 -[NSScreen localizedName] is available:
NSLog(#"Name of main display is %#", NSScreen.mainScreen.localizedName);
NSString* screenNameForDisplay(CGDirectDisplayID displayID)
{
NSString *screenName = nil;
io_service_t service = IOServicePortFromCGDisplayID(displayID);
if (service)
{
NSDictionary *deviceInfo = (NSDictionary *)IODisplayCreateInfoDictionary(service, kIODisplayOnlyPreferredName);
NSDictionary *localizedNames = [deviceInfo objectForKey:[NSString stringWithUTF8String:kDisplayProductName]];
if ([localizedNames count] > 0) {
screenName = [[localizedNames objectForKey:[[localizedNames allKeys] objectAtIndex:0]] retain];
}
[deviceInfo release];
}
return [screenName autorelease];
}
In my app I'me getting responses from the server and I have to check that I don't create duplicate objects in the NSArray which contains NSDictionaries. Now to check if the objects exists I do this:
for (int i = 0; i < appDelegate.currentUser.userSiteDetailsArray.count; i++){
NSDictionary *tmpDictionary = [appDelegate.currentUser.userSiteDetailsArray objectAtIndex:i];
if ([[tmpDictionary valueForKey:#"webpropID"] isEqualToString:tmpWebproperty.identifier]){
needToCheck = NO;
}
if (i == appDelegate.currentUser.userSiteDetailsArray.count - 1 && ![[tmpDictionary valueForKey:#"webpropID"] isEqualToString:tmpWebproperty.identifier] && needToCheck){
// It means it's the last object we've iterated through and needToCheck is still = YES;
//Doing stuff here
}
}
I set up a BOOL value because this iteration goes numerous times inside a method and I can't use return to stop it. I think there is a better way to perform this check and I would like to hear your suggestions about it.
BOOL needToCheck = YES;
for (int i = 0; i < appDelegate.currentUser.userSiteDetailsArray.count; i++){
NSDictionary *tmpDictionary = [appDelegate.currentUser.userSiteDetailsArray objectAtIndex:i];
if ([[tmpDictionary valueForKey:#"webpropID"] isEqualToString:tmpWebproperty.identifier]){
needToCheck = NO;
break;
}
}
if (needToCheck) {
//Doing stuff here
}
But, as others have said, you can maybe keep a "summary" in a separate NSSet that you check first, vs spinning through all the dictionaries.
NSDictionary *previousThing = nil;
for (NSDictionary *thing in appDelegate.currentUser.userSiteDetailsArray) {
if ([thing[#"webpropID"] isEqualToString:newWebPropertyIdentifier]) {
previousThing = thing;
break;
}
}
if (previousThing == nil) {
// no previous thing
} else {
// duplicate
}
I would like to obtain a list of drives that are unmountable/ejectable using Cocoa/Objective-C under OS X.
I was hoping that NSWorkspace getFileSystemInfoForPath::::: would help me:
NSArray* listOfMedia = [[NSWorkspace sharedWorkspace] mountedLocalVolumePaths];
NSLog(#"%#", listOfMedia);
for (NSString* volumePath in listOfMedia)
{
BOOL isRemovable = NO;
BOOL isWritable = NO;
BOOL isUnmountable = NO;
NSString* description = [NSString string];
NSString* type = [NSString string];
BOOL result = [[NSWorkspace sharedWorkspace] getFileSystemInfoForPath:volumePath
isRemovable:&isRemovable
isWritable:&isWritable
isUnmountable:&isUnmountable
description:&description
type:&type];
NSLog(#"Result:%i Volume: %#, Removable:%i, W:%i, Unmountable:%i, Desc:%#, type:%#", result, volumePath, isRemovable, isWritable, isUnmountable, description, type);
}
Output:
...
Result:1 Volume: /Volumes/LR Photos, Removable:0, W:1, Unmountable:0, Desc:hfs, type:hfs
...
"LR Photos" is an external drive (connected via Thunderbolt) that should be removable and/or unmountable (or, at least I think it should be). :)
Should I be going about this a different way?
Thanks in advance!
You can use diskArbitration framework.
#import <DiskArbitration/DiskArbitration.h>
+(NSMutableArray *)getListOfEjectableMedia
{
NSArray *mountedRemovableMedia = [[NSFileManager defaultManager] mountedVolumeURLsIncludingResourceValuesForKeys:nil options:NSVolumeEnumerationSkipHiddenVolumes];
NSMutableArray *result = [NSMutableArray array];
for(NSURL *volURL in mountedRemovableMedia)
{
int err = 0;
DADiskRef disk;
DASessionRef session;
CFDictionaryRef descDict;
session = DASessionCreate(NULL);
if (session == NULL) {
err = EINVAL;
}
if (err == 0) {
disk = DADiskCreateFromVolumePath(NULL,session,(CFURLRef)volURL);
if (session == NULL) {
err = EINVAL;
}
}
if (err == 0) {
descDict = DADiskCopyDescription(disk);
if (descDict == NULL) {
err = EINVAL;
}
}
if (err == 0) {
CFTypeRef mediaEjectableKey = CFDictionaryGetValue(descDict,kDADiskDescriptionMediaEjectableKey);
CFTypeRef deviceProtocolName = CFDictionaryGetValue(descDict,kDADiskDescriptionDeviceProtocolKey);
if (mediaEjectableKey != NULL)
{
BOOL op = CFEqual(mediaEjectableKey, CFSTR("0")) || CFEqual(deviceProtocolName, CFSTR("USB"));
if (op) {
[result addObject:volURL];
}
}
}
if (descDict != NULL) {
CFRelease(descDict);
}
if (disk != NULL) {
CFRelease(disk);
}
if (session != NULL) {
CFRelease(session);
}
}
return result;
}
Unfortunately getFileSystemInfoForPath: is not really the right way to do this. What removable means is that the volume is on removable media such as a CD or DVD. In practice unmountable seems to give the same results as removable. See for example, this post on results using getFileSystemInfoForPath. So unless you want to simply know if a volume is on removable media, you'll need to use another technique.
What you really want to check is the connection bus type of the volume. Firewire, USB, Thunderbolt, etc. are unmountable in the sense you mean. You can see this information in Disk Utility if you select the volume and push the "Info" button under "Connection Bus". Getting this information programmatically is much harder and as far as I can tell is only possible using the IOKit. Details are in Apple's documentation on Accessing Hardware from Applications.
you can use command line version of Disk Utility app that is "diskutil", run it with parameter "list" and pipe output and get it in your program ( don't need to use cocoa ).