Add / Remove Workspace on Mac Programmatically

I have a pretty simple question. How would I programmatically add / remove workspaces found in mission management. I saw this post here about programmatically translating to another space, and I think it could be something similar to the answer using CGSPrivate.h. I don’t have to worry about private frameworks as it is not part of the application store.

EDIT: I also saw a message about changing com.apple.spaces.plistand adding workspaces, but I have no idea how I would add that since the dict has UUID and other things.

+4
source share
3 answers

So far, Mission Control has a Dock availability hierarchy (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 delete button.

My test application:

- (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);
}
+2
source

Similarly Willeke, I was able to accomplish this after hours of code. Here is my code, then I will 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;
}

Now go through the code step by step

To remove workspaces, the first method removeAllWorkspacesis the starting point.

This code gets the number of workspaces opened from the file com.apple.spaces.plist, and then sets the variable workspacesToRemove. This variable is important for the loop, since it is difficult to execute for-loopwhen there are chains of methods (as I call them).

, CGEvents. , , .

, , rectForWorkspaces.

, , .

, . , , : Initial calculation

, 145 ( ) , .

, ( 1) .

FWI: , , , .

!

.

(openWorkspaces:(NSInteger)numberToOpen), , , . .

+1

, apple script - . "" :

?

0

Source: https://habr.com/ru/post/1625917/


All Articles