我正在尝试将以下代码用作macOS命令行工具。重要的是,这不是一个Cocoa应用程序,所以这不是一个选项。
这段代码在同一个项目中使用Cocoa App目标完美地工作,并检测兼容的控制器,但是当作为命令行工具目标运行时,什么也不发生,API显示没有连接的控制器。
很明显,其中一些是人为的。这只是我能归结为的最简单的事情,并在它实际工作时对发生的事情有一些指示。
#import <Cocoa/Cocoa.h>
#import <GameController/GameController.h>
int main( int argc, const char * argv[] )
{
@autoreleasepool
{
NSApplication * application = [NSApplication sharedApplication];
NSNotificationCenter * center = [NSNotificationCenter defaultCenter];
[center addObserverForName: GCControllerDidConnectNotification
object: nil
queue: nil
usingBlock: ^(NSNotification * note) {
GCController * controller = note.object;
printf( "ATTACHED: %s\n", controller.vendorName.UTF8String );
}
];
[application finishLaunching];
bool shouldKeepRunning = true;
while (shouldKeepRunning)
{
printf( "." );
while (true)
{
NSEvent * event = [application
nextEventMatchingMask: NSEventMaskAny
untilDate: nil
inMode: NSDefaultRunLoopMode
dequeue: YES];
if (event == NULL)
{
break;
}
else
{
[application sendEvent: event];
}
}
usleep( 100 * 1000 );
}
}
return 0;
}我猜这与Cocoa应用程序的设置或事件循环的处理方式有关。或者可能有一些内部触发器初始化GameController框架。API似乎没有任何显式的方法来初始化它。
https://developer.apple.com/documentation/gamecontroller?language=objc
有没有人能解释一下我是如何让它工作的?
归根结底,这段代码确实需要在Core Foundation捆绑包中工作,所以如果它能与Core Foundation runloop一起工作,那将是最理想的。
-编辑--
我已经做了一个测试项目来更清楚地说明这个问题。有两个构建目标。Cocoa应用程序构建目标工作并接收控制器连接事件。另一个构建目标,只是一个简单的CLI应用程序,不起作用。它们都使用相同的源文件。它还包括两个代码路径,其中一个是传统的NSApp运行,第二个是上面的手动事件循环。结果是一样的。
https://www.dropbox.com/s/a6fw3nuegq7bg8x/ControllerTest.zip?dl=0
发布于 2019-08-09 00:00:40
尽管每个线程都会创建一个run循环(对于Cocoa应用程序是NSRunLoop)来处理输入事件,但该循环不会自动启动。下面的代码使其与[application run]调用一起运行。当运行循环处理适当的事件时,将引发通知。我将观察器安装在Application委托中,只是为了确保所有其他系统都在此时完成了初始化。
#import <Cocoa/Cocoa.h>
#import <GameController/GameController.h>
@interface AppDelegate : NSObject <NSApplicationDelegate> @end
@implementation AppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)notification {
NSNotificationCenter * center = [NSNotificationCenter defaultCenter];
[center addObserverForName: GCControllerDidConnectNotification
object: nil
queue: nil
usingBlock: ^(NSNotification * note) {
GCController * controller = note.object;
printf( "ATTACHED: %s\n", controller.vendorName.UTF8String );
}
];
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSApplication * application = [NSApplication sharedApplication]; // You can get rid of the variable and just use the global NSApp below instead
AppDelegate *delegate = [[AppDelegate alloc] init];
[application setDelegate:delegate];
[application run];
}
return 0;
}更新对不起,我误解了问题。上面的代码用于连接和断开控制器,但它不能使用应用程序启动时已连接的设备正确初始化[GCController controllers]数组。
正如你所指出的,互联设备在Cocoa应用程序上发送带有相同代码的通知,但不是在命令行应用程序上。不同之处在于,Cocoa应用程序接收didBecomeActive通知,这会导致私有_GCControllerManager (负责GameControllerDaemon发布的NSXPCConnection的对象)接收填充controllers数组的CBApplicationDidBecomeActive消息。
无论如何,我尝试激活命令行应用程序,以便它路由这些消息,但这并不起作用;应用程序需要在启动时尽早发送didBecomeActive消息。
我需要的是访问私有_GCGameController对象,但是我不知道谁拥有它,所以我不能直接引用它。
所以在最后,我选择了swizzling方法。下面的代码更改了在终端应用程序中初始化时调用的最后一个方法_GCGameController startIdleWatchTimer,因此它随后会发送CBApplicationDidBecomeActive。
我知道这不是一个很好的解决方案,使用各种苹果的内部代码,但也许它可以帮助某人获得更好的东西。将以下代码添加到前面的代码中:
#import <objc/runtime.h>
@interface _GCControllerManager : NSObject
-(void) CBApplicationDidBecomeActive;
-(void) startIdleWatchTimer;
@end
@implementation _GCControllerManager (Extras)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
SEL originalSelector = @selector(startIdleWatchTimer);
SEL swizzledSelector = @selector(myStartIdleWatchTimer);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
BOOL didAddMethod =
class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
- (void) myStartIdleWatchTimer {
[self myStartIdleWatchTimer];
[self CBApplicationDidBecomeActive];
}
@end发布于 2019-11-09 02:38:38
我用下面的main.m文件实现了这个功能:
#import <AppKit/AppKit.h>
#import <GameController/GameController.h>
@interface AppDelegate : NSObject<NSApplicationDelegate> @end
@implementation AppDelegate
- (void) applicationDidFinishLaunching: (NSNotification*) notification {
[NSApp stop: nil]; // Allows [app run] to return
}
@end
int main() {
NSApplication* app = [NSApplication sharedApplication];
[app setActivationPolicy: NSApplicationActivationPolicyRegular];
[app setDelegate: [[AppDelegate alloc] init]];
[app run];
// 1 with a DualShock 4 plugged in
printf("controllers %lu\n", [[GCController controllers] count]);
// Do stuff here
return 0;
}编译方式:clang -framework AppKit -framework GameController main.m
我不知道为什么,但我需要在构建输出目录中有一个Info.plist文件。如果没有它,控制器数组就不会被填充。这是我的整个文件:
<dict>
<key>CFBundleIdentifier</key>
<string>your.bundle.id</string>
</dict>我不确定提供控制器可能会有什么影响,但如果它存在,我就可以正常运行a.out可执行文件,并得到我的Info.plist数组。
https://stackoverflow.com/questions/55226373
复制相似问题