Search code examples
macoscocoaworkspacemission-control

Add/remove workspace to mac programmatically


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.


Solution

  • 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);
    }