Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 529
Minebomber Avatar asked Oct 18 '22 17:10

Minebomber


2 Answers

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);
}
like image 121
Willeke Avatar answered Oct 21 '22 06:10

Willeke


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

Now lets go through the code step by step

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: Rect calculation

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.

Yay complicated closing over!

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.

like image 35
Minebomber Avatar answered Oct 21 '22 08:10

Minebomber