编程方式在Mac上添加/删除工作区

5
我有一个非常简单的问题。我如何通过编程添加/删除在任务控制中找到的工作区。我看过这篇文章here关于编程更改到另一个空间,我认为它可能类似于答案,使用CGSPrivate.h。我不需要担心私有框架,因为它不会出现在应用商店上。
编辑:我还看到了一篇关于修改com.apple.spaces.plist并添加工作区的帖子,但我不知道如何添加,因为dict有UUID和其他内容。

可以通过模拟按键和点击来添加和删除工作区。您介意一些副作用,例如在任务控制之间来回闪烁吗? - Willeke
@willeke 我不介意,目前正在研究一个点击算法。感谢您的帮助。 - Minebomber
3个回答

2

在我的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

工作区按钮的位置在删除按钮的中间。

我的测试应用:

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

哇,那很复杂。 - Minebomber
我已经将问题缩小到了- (AXUIElementRef)copyAXUIElementFrom:(AXUIElementRef)theContainer role:(CFStringRef)theRole atIndex:(NSInteger)theIndex方法。 - Minebomber
任务控制在El Capitan下更加动态,而选项键技巧不再奏效。我喜欢挑战。 - Willeke
代码需要访问计算机(系统偏好设置、安全性与隐私、隐私、辅助功能),并且缺乏错误检查。我编辑了我的答案。现在的方法会检查当前进程是否是受信任的辅助功能客户端。删除方法将鼠标移动到屏幕顶部,以便看到带有删除 X 的大按钮。 - Willeke
AXUIElement是辅助功能API的一部分,它被VoiceOver和GUI脚本等技术所使用。尝试使用Accessibility Inspector,它在Xcode的开发者工具中。 - Willeke
显示剩余3条评论

1

Willeke 一样,我在花费了许多时间编写代码后成功了。这是我的代码,接下来我将为任何未来遇到此问题的人解释它的作用。

.h 文件中:

我的代码在 AppDelegate 中(这是一个菜单栏应用程序)。

@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

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

现在让我们逐步了解代码

对于删除工作区,第一个方法removeAllWorkspaces是非常重要的起点。

这段代码从com.apple.spaces.plist文件中获取打开的工作区数量,然后设置变量workspacesToRemove。这个变量很重要,因为当有方法链时(我称之为),使用for-loop很难。

接下来,我调用一个方法通过CGEvents打开任务控制器。然后我将鼠标移动到屏幕的顶部角落,以确保工作区图标正确居中。

接下来,代码使用rectForWorkspaces方法确定最右边工作空间的关闭按钮的位置。

虽然这是一个相当简单的方法,但它是发生的主要部分。

它计算出任务控制器中工作区的矩形位置。下面是代表它所计算的图像: Rect calculation

我然后取这个矩形,减去145(工作区图标宽度),并在弹出关闭按钮时点击。
这部分循环直到所有工作区(除了1个)都关闭。
FWI:将其拆分为多个方法的原因是我可以返回到特定的方法并在延迟后执行方法而不阻塞线程。
耶复杂的关闭!
添加工作区要容易得多。
只有一个方法(openWorkspaces:(NSInteger)numberToOpen ),它打开任务控件,移动鼠标到位置,并单击多次,直到添加所有工作区。非常简单。

0

我觉得用那个方法删除工作区不会很容易。 - Minebomber
同意。就像我说的那样,这是我能想到的最接近的,但并不完全符合你的要求。 - Rhys Lewis

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接