I have a fairly simple question. How would I programmatically add/remove the workspaces found in mission control. I have seen this post here about changing to another space programmatically, and I think that it could be something similar to the answer, using CGSPrivate.h
. I don't need to worry about private frameworks, as it's not going on the app store.
EDIT: I also saw a post about modifying the com.apple.spaces.plist
and adding workspaces, but I have no Idea how I would add that, as the dict has UUID and other things.
While in Mission Control this is the Accessibility Hierarchy of Dock (on my Mac, OS X 10.10):
Role Position Title Value Description
AXList 632.000000, 1136.000000 (null) (null) (null)
AXDockItem 636.300049, 1138.000000 Finder (null) (null)
AXDockItem 688.300049, 1138.000000 Firefox (null) (null)
…
AXDockItem 1231.699951, 1138.000000 Trash (null) (null)
AXGroup 0.000000, 0.000000 (null) (null) (null)
AXGroup 20.000000, 227.000000 (null) (null) exposéd windows
AXList 0.000000, -2.000000 (null) (null) (null)
AXButton 592.000000, 20.000000 Desktop 1 (null) select Desktop 1
AXButton 864.000000, 20.000000 Desktop 2 (null) select Desktop 2
AXButton 1136.000000, 20.000000 Desktop 3 (null) select Desktop 3
AXButton 1824.000000, 20.000000 (null) (null) add desktop
The location of the workspace buttons is the middle of the remove button.
My test app:
- (AXUIElementRef)copyAXUIElementFrom:(AXUIElementRef)theContainer role:(CFStringRef)theRole atIndex:(NSInteger)theIndex {
AXUIElementRef aResultElement = NULL;
CFTypeRef aChildren;
AXError anAXError = AXUIElementCopyAttributeValue(theContainer, kAXChildrenAttribute, &aChildren);
if (anAXError == kAXErrorSuccess) {
NSUInteger anIndex = -1;
for (id anElement in (__bridge NSArray *)aChildren) {
if (theRole) {
CFTypeRef aRole;
anAXError = AXUIElementCopyAttributeValue((__bridge AXUIElementRef)anElement, kAXRoleAttribute, &aRole);
if (anAXError == kAXErrorSuccess) {
if (CFStringCompare(aRole, theRole, 0) == kCFCompareEqualTo)
anIndex++;
CFRelease(aRole);
}
}
else
anIndex++;
if (anIndex == theIndex) {
aResultElement = (AXUIElementRef)CFRetain((__bridge CFTypeRef)(anElement));
break;
}
}
CFRelease(aChildren);
}
return aResultElement;
}
- (IBAction)addWorkspace:(id)sender {
if (!AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)@{(__bridge NSString *)kAXTrustedCheckOptionPrompt:@YES}))
return;
// type control-arrow-up
CGEventRef anEvent = CGEventCreateKeyboardEvent(NULL, 0x7E, true);
CGEventSetFlags(anEvent, kCGEventFlagMaskControl);
CGEventPost(kCGHIDEventTap, anEvent);
CFRelease(anEvent);
[NSThread sleepForTimeInterval:0.05];
anEvent = CGEventCreateKeyboardEvent(NULL, 0x7E, false);
CGEventSetFlags(anEvent, kCGEventFlagMaskControl);
CGEventPost(kCGHIDEventTap, anEvent);
CFRelease(anEvent);
[NSThread sleepForTimeInterval:0.05];
// option down
anEvent = CGEventCreateKeyboardEvent(NULL, 0x3A, true);
CGEventPost(kCGHIDEventTap, anEvent);
[NSThread sleepForTimeInterval:0.05];
// click on the + button
NSArray *anArray = [NSRunningApplication runningApplicationsWithBundleIdentifier:@"com.apple.dock"];
AXUIElementRef anAXDockApp = AXUIElementCreateApplication([[anArray objectAtIndex:0] processIdentifier]);
CFTypeRef aGroup = [self copyAXUIElementFrom:anAXDockApp role:kAXGroupRole atIndex:0];
CFTypeRef aButton = [self copyAXUIElementFrom:aGroup role:kAXButtonRole atIndex:0];
CFRelease(aGroup);
if (aButton) {
AXError anAXError = AXUIElementPerformAction(aButton, kAXPressAction);
CFRelease(aButton);
}
// option up
anEvent = CGEventCreateKeyboardEvent(NULL, 0x3A, false);
CGEventPost(kCGHIDEventTap, anEvent);
[NSThread sleepForTimeInterval:0.05];
// type escape
anEvent = CGEventCreateKeyboardEvent(NULL, 0x35, true);
CGEventPost(kCGHIDEventTap, anEvent);
CFRelease(anEvent);
[NSThread sleepForTimeInterval:0.05];
anEvent = CGEventCreateKeyboardEvent(NULL, 0x35, false);
CGEventPost(kCGHIDEventTap, anEvent);
CFRelease(anEvent);
}
- (IBAction)removeWorkspace:(id)sender {
if (!AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)@{(__bridge NSString *)kAXTrustedCheckOptionPrompt:@YES}))
return;
// type control-arrow-up
CGEventRef anEvent = CGEventCreateKeyboardEvent(NULL, 0x7E, true);
CGEventSetFlags(anEvent, kCGEventFlagMaskControl);
CGEventPost(kCGHIDEventTap, anEvent);
CFRelease(anEvent);
[NSThread sleepForTimeInterval:0.05];
anEvent = CGEventCreateKeyboardEvent(NULL, 0x7E, false);
CGEventSetFlags(anEvent, kCGEventFlagMaskControl);
CGEventPost(kCGHIDEventTap, anEvent);
CFRelease(anEvent);
[NSThread sleepForTimeInterval:0.05];
// move mouse to the top of the screen
CGPoint aPoint;
aPoint.x = 10.0;
aPoint.y = 10.0;
anEvent = CGEventCreateMouseEvent(NULL, kCGEventMouseMoved, aPoint, 0);
CGEventPost(kCGHIDEventTap, anEvent);
CFRelease(anEvent);
[NSThread sleepForTimeInterval:0.05];
// option down
anEvent = CGEventCreateKeyboardEvent(NULL, 0x3A, true);
CGEventPost(kCGHIDEventTap, anEvent);
[NSThread sleepForTimeInterval:0.05];
// option down
anEvent = CGEventCreateKeyboardEvent(NULL, 0x3A, true);
CGEventPost(kCGHIDEventTap, anEvent);
[NSThread sleepForTimeInterval:0.05];
// click at the location of the workspace
NSArray *anArray = [NSRunningApplication runningApplicationsWithBundleIdentifier:@"com.apple.dock"];
AXUIElementRef anAXDockApp = AXUIElementCreateApplication([[anArray objectAtIndex:0] processIdentifier]);
CFTypeRef aGroup = [self copyAXUIElementFrom:anAXDockApp role:kAXGroupRole atIndex:0];
CFTypeRef aList = [self copyAXUIElementFrom:aGroup role:kAXListRole atIndex:0];
CFRelease(aGroup);
CFTypeRef aButton = [self copyAXUIElementFrom:aList role:kAXButtonRole atIndex:1]; // index of the workspace
CFRelease(aList);
if (aButton) {
CFTypeRef aPosition;
AXError anAXError = AXUIElementCopyAttributeValue(aButton, kAXPositionAttribute, &aPosition);
if (anAXError == kAXErrorSuccess) {
AXValueGetValue(aPosition, kAXValueCGPointType, &aPoint);
CFRelease(aPosition);
// click
anEvent = CGEventCreateMouseEvent(NULL, kCGEventLeftMouseDown, aPoint, kCGMouseButtonLeft);
CGEventPost(kCGHIDEventTap, anEvent);
CFRelease(anEvent);
[NSThread sleepForTimeInterval:0.05];
anEvent = CGEventCreateMouseEvent(NULL, kCGEventLeftMouseUp, aPoint, kCGMouseButtonLeft);
CGEventPost(kCGHIDEventTap, anEvent);
CFRelease(anEvent);
[NSThread sleepForTimeInterval:0.05];
CFRelease(aButton);
}
}
// option up
anEvent = CGEventCreateKeyboardEvent(NULL, 0x3A, false);
CGEventPost(kCGHIDEventTap, anEvent);
[NSThread sleepForTimeInterval:0.05];
// type escape
anEvent = CGEventCreateKeyboardEvent(NULL, 0x35, true);
CGEventPost(kCGHIDEventTap, anEvent);
CFRelease(anEvent);
[NSThread sleepForTimeInterval:0.05];
anEvent = CGEventCreateKeyboardEvent(NULL, 0x35, false);
CGEventPost(kCGHIDEventTap, anEvent);
CFRelease(anEvent);
}
Similar to Willeke
I was able to accomplish this after many hour of code. Here is my code, then I'll explain what it does for any future people who come across this.
In .h
My code is in AppDelegate (it is a menubar app).
@interface AppDelegate : NSObject <NSApplicationDelegate>
{
...
// Workspace mutations vars
NSInteger workspacesToRemove; // Used in removing workspaces (as
loop)
}
// Define constants for sizes
#define kWORKSPACE_WIDTH 145
#define kWORKSPACE_HEIGHT 90
#define kWORKSPACE_SPACING 30
In .m
- (void)removeAllWorkspaces
{
NSDictionary *spacesPlist = [NSDictionary dictionaryWithContentsOfFile:[NSHomeDirectory() stringByAppendingPathComponent:@"Library/Preferences/com.apple.spaces.plist"]];
NSDictionary *spacesDisplayConfig = [spacesPlist objectForKey:[[spacesPlist allKeys] objectAtIndex:0]];
NSArray *spaceProperties = [spacesDisplayConfig objectForKey:@"Space Properties"];
NSInteger numberOfWorkspaces = [spaceProperties count];
NSLog(@"Number of workspaces: %ld", (long)numberOfWorkspaces);
// Set counter
workspacesToRemove = numberOfWorkspaces;
[self openMissionControl];
}
#pragma mark Open/Close step methods
- (void)openMissionControl
{
CGEventSourceRef src =
CGEventSourceCreate(kCGEventSourceStateHIDSystemState);
CGEventRef cntd = CGEventCreateKeyboardEvent(src, 0x3B, YES);
CGEventRef cntu = CGEventCreateKeyboardEvent(src, 0x3B, NO);
CGEventRef upd = CGEventCreateKeyboardEvent(src, 0x7E, YES);
CGEventRef upu = CGEventCreateKeyboardEvent(src, 0x7E, NO);
/*
*/
CGEventSetFlags(upd, kCGEventFlagMaskControl);
CGEventSetFlags(upu, kCGEventFlagMaskControl);
CGEventTapLocation loc = kCGHIDEventTap; // kCGSessionEventTap also works
CGEventPost(loc, cntd);
CGEventPost(loc, upd);
CGEventPost(loc, upu);
CGEventPost(loc, cntu);
CFRelease(cntd);
CFRelease(cntu);
CFRelease(upd);
CFRelease(upu);
[self performSelector:@selector(moveMouseToUpdateMissionControl) withObject:nil afterDelay:1];
}
- (void)moveMouseToUpdateMissionControl
{
CGEventPost(kCGHIDEventTap, CGEventCreateMouseEvent(NULL, kCGEventMouseMoved, CGPointMake([[NSScreen mainScreen] frame].size.width - 10, 10), kCGMouseButtonLeft));
[self performSelector:@selector(moveMouseToCloseRightmostWorkspace) withObject:nil afterDelay:1];
}
- (void)moveMouseToCloseRightmostWorkspace
{
NSRect workspaceRect = [self rectForWorkspaces];
NSInteger closeX = (workspaceRect.origin.x + workspaceRect.size.width) - kWORKSPACE_WIDTH;
CGPoint closePoint = CGPointMake(closeX, workspaceRect.origin.y);
// Move mouse to point
CGEventRef mouseMove = CGEventCreateMouseEvent(NULL, kCGEventMouseMoved, closePoint, kCGMouseButtonLeft);
CGEventPost(kCGHIDEventTap, mouseMove);
CFRelease(mouseMove);
// Click
[self performSelector:@selector(clickMouseAtPoint:) withObject:[NSValue valueWithPoint:closePoint] afterDelay:2]; // Must be equal or greater 1.5
}
- (void)clickMouseAtPoint:(NSValue *)pointValue
{
CGPoint clickPoint = [pointValue pointValue];
// Click
CGEventPost(kCGHIDEventTap, CGEventCreateMouseEvent(NULL, kCGEventLeftMouseDown, clickPoint, kCGMouseButtonLeft));
CGEventPost(kCGHIDEventTap, CGEventCreateMouseEvent(NULL, kCGEventLeftMouseUp, clickPoint, kCGMouseButtonLeft));
workspacesToRemove--;
NSLog(@"%ld", (long)workspacesToRemove);
if (workspacesToRemove > 1) {
[self performSelector:@selector(moveMouseToCloseRightmostWorkspace) withObject:nil afterDelay:2];
} else {
[self performSelector:@selector(closeMissionControl) withObject:nil afterDelay:1];
}
}
- (void)closeMissionControl
{
CGEventSourceRef src =
CGEventSourceCreate(kCGEventSourceStateHIDSystemState);
CGEventRef cntd = CGEventCreateKeyboardEvent(src, 0x3B, YES);
CGEventRef cntu = CGEventCreateKeyboardEvent(src, 0x3B, NO);
CGEventRef upd = CGEventCreateKeyboardEvent(src, 0x7E, YES);
CGEventRef upu = CGEventCreateKeyboardEvent(src, 0x7E, NO);
CGEventSetFlags(upd, kCGEventFlagMaskControl);
CGEventSetFlags(upu, kCGEventFlagMaskControl);
CGEventTapLocation loc = kCGHIDEventTap; // kCGSessionEventTap also works
CGEventPost(loc, cntd);
CGEventPost(loc, upd);
CGEventPost(loc, upu);
CGEventPost(loc, cntu);
CFRelease(cntd);
CFRelease(cntu);
CFRelease(upd);
CFRelease(upu);
}
#pragma mark
#pragma mark Adding Workspaces
- (void)openWorkspaces:(NSInteger)numberToOpen
{
// Open Mission control
CGEventSourceRef src =
CGEventSourceCreate(kCGEventSourceStateHIDSystemState);
CGEventRef cntd = CGEventCreateKeyboardEvent(src, 0x3B, YES);
CGEventRef cntu = CGEventCreateKeyboardEvent(src, 0x3B, NO);
CGEventRef upd = CGEventCreateKeyboardEvent(src, 0x7E, YES);
CGEventRef upu = CGEventCreateKeyboardEvent(src, 0x7E, NO);
/*
*/
CGEventSetFlags(upd, kCGEventFlagMaskControl);
CGEventSetFlags(upu, kCGEventFlagMaskControl);
CGEventTapLocation loc = kCGHIDEventTap; // kCGSessionEventTap also works
CGEventPost(loc, cntd);
CGEventPost(loc, upd);
CGEventPost(loc, upu);
CGEventPost(loc, cntu);
[NSThread sleepForTimeInterval:2];
// Move mouse to point
CGEventRef mouseMove = CGEventCreateMouseEvent(NULL, kCGEventMouseMoved, CGPointMake([[NSScreen mainScreen] frame].size.width - 10, 10), kCGMouseButtonLeft);
CGEventPost(kCGHIDEventTap, mouseMove);
CFRelease(mouseMove);
for (NSInteger i = 0; i < numberToOpen; i++) {
// Add as many times as needed
CGEventPost(kCGHIDEventTap, CGEventCreateMouseEvent(NULL, kCGEventLeftMouseDown, CGPointMake([[NSScreen mainScreen] frame].size.width - 10, 10), kCGMouseButtonLeft));
CGEventPost(kCGHIDEventTap, CGEventCreateMouseEvent(NULL, kCGEventLeftMouseUp, CGPointMake([[NSScreen mainScreen] frame].size.width - 10, 10), kCGMouseButtonLeft));
[NSThread sleepForTimeInterval:1];
}
CGEventPost(loc, cntd);
CGEventPost(loc, upd);
CGEventPost(loc, upu);
CGEventPost(loc, cntu);
CFRelease(cntd);
CFRelease(cntu);
CFRelease(upd);
CFRelease(upu);
}
- (NSRect)rectForWorkspaces
{
NSDictionary *spacesPlist = [NSDictionary dictionaryWithContentsOfFile:[NSHomeDirectory() stringByAppendingPathComponent:@"Library/Preferences/com.apple.spaces.plist"]];
NSDictionary *spacesDisplayConfig = [spacesPlist objectForKey:[[spacesPlist allKeys] objectAtIndex:0]];
NSArray *spaceProperties = [spacesDisplayConfig objectForKey:@"Space Properties"];
NSInteger numberOfWorkspaces = [spaceProperties count];
NSInteger totalSpacing = (numberOfWorkspaces - 1) * kWORKSPACE_SPACING;
NSInteger totalLengthOfWorkspaces = numberOfWorkspaces * kWORKSPACE_WIDTH;
NSInteger totalRectWidth = totalSpacing + totalLengthOfWorkspaces;
NSRect workspaceRect = NSMakeRect(0, 0, totalRectWidth, kWORKSPACE_HEIGHT);
// Calculate center x or screen
NSInteger screenCenter = [[NSScreen mainScreen] frame].size.width / 2;
workspaceRect.origin.x = screenCenter - (workspaceRect.size.width / 2);
workspaceRect.origin.y = kWORKSPACE_SPACING;
return workspaceRect;
}
For removing workspaces, the first method removeAllWorkspaces
, is the very starting point.
This code gets the number of workspaces open from the com.apple.spaces.plist
file and then sets the variable workspacesToRemove
. This variable is important for looping as it is hard to do a for-loop
when there are method chains (as I call them).
Next, I call a method to open mission control by doing CGEvents
. Then I move the mouse to the top corner of the screen to make sure the workspace icons are centered properly.
Next, the code determines the position of the close button of the rightmost workspace using the rectForWorkspaces
method.
This is a pretty simple method, but it is the main part of what happens.
It calculated the rectangle of where the workspaces are going to be in mission control. Here is an image representing what it calculates:
I then take this rect, subtract 145 (workspace icon width), and click in the close button when it pops up.
This part loops until all workspaces (except 1) are closed.
FWI: The reason it is split into many methods is so I can loop back to a specific one and execute the methods after delays without blocking threads.
The adding of workspaces is a lot easier.
It is only one method (openWorkspaces:(NSInteger)numberToOpen
), and it opens mission control, moves mouse to position, and clicks number of times until all the workspaces have been added. Very simple.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With