#ifdef __APPLE__ #import #include "config.hpp" #include "connector_service.hpp" #include "platform.hpp" using connector::ConnectorConfig; using connector::ConnectorService; @interface ConnectorAppDelegate : NSObject @property(nonatomic, strong) NSWindow* window; @property(nonatomic, strong) NSTextField* pathField; @property(nonatomic, strong) NSTextField* statusLabel; @property(nonatomic, strong) NSArray* pathControls; @property(nonatomic, strong) NSButton* connectionButton; @property(nonatomic, assign) ConnectorService* service; @property(nonatomic, copy) NSString* startupError; @end @implementation ConnectorAppDelegate - (NSTextField*)label:(NSString*)text frame:(NSRect)frame size:(CGFloat)size color:(NSColor*)color { NSTextField* label = [[NSTextField alloc] initWithFrame:frame]; label.stringValue = text; label.editable = NO; label.bezeled = NO; label.drawsBackground = NO; label.font = [NSFont systemFontOfSize:size]; label.textColor = color; return label; } - (NSButton*)button:(NSString*)title frame:(NSRect)frame action:(SEL)action { NSButton* button = [[NSButton alloc] initWithFrame:frame]; button.title = title; button.bezelStyle = NSBezelStyleRounded; button.target = self; button.action = action; return button; } - (void)applicationDidFinishLaunching:(NSNotification*)notification { (void)notification; self.window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 620, 326) styleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable backing:NSBackingStoreBuffered defer:NO]; self.window.title = @"Spine粒子连接器"; self.window.delegate = self; [self.window center]; NSView* content = self.window.contentView; [content addSubview:[self label:@"Spine粒子连接器" frame:NSMakeRect(28, 268, 560, 34) size:24 color:NSColor.labelColor]]; [content addSubview:[self label:@"应用打开且连接已启动时,网页可调用本机 Spine 生成工程文件。" frame:NSMakeRect(29, 237, 560, 24) size:14 color:NSColor.secondaryLabelColor]]; self.statusLabel = [self label:@"" frame:NSMakeRect(29, 201, 560, 24) size:14 color:NSColor.systemGreenColor]; [content addSubview:self.statusLabel]; NSTextField* pathLabel = [self label:@"Spine.com 路径" frame:NSMakeRect(29, 162, 130, 24) size:14 color:NSColor.labelColor]; [content addSubview:pathLabel]; self.pathField = [[NSTextField alloc] initWithFrame:NSMakeRect(29, 126, 390, 32)]; self.pathField.placeholderString = @"留空时自动检测 Spine"; const ConnectorConfig config = connector::load_config(); self.pathField.stringValue = [NSString stringWithUTF8String:config.spine_executable.c_str()]; [content addSubview:self.pathField]; NSButton* browse = [self button:@"选择 Spine.com" frame:NSMakeRect(430, 124, 160, 36) action:@selector(selectSpine:)]; [content addSubview:browse]; NSButton* automatic = [self button:@"恢复自动检测" frame:NSMakeRect(29, 78, 130, 36) action:@selector(clearPath:)]; [content addSubview:automatic]; NSButton* save = [self button:@"保存路径" frame:NSMakeRect(460, 78, 130, 36) action:@selector(savePath:)]; save.keyEquivalent = @"\r"; [content addSubview:save]; self.pathControls = @[pathLabel, self.pathField, browse, automatic, save]; self.connectionButton = [self button:@"断开连接" frame:NSMakeRect(460, 25, 130, 36) action:@selector(toggleConnection:)]; [content addSubview:self.connectionButton]; [self refreshStatus:nil]; [self.window makeKeyAndOrderFront:nil]; [NSApp activateIgnoringOtherApps:YES]; } - (void)refreshStatus:(id)sender { (void)sender; self.connectionButton.title = self.service && self.service->running() ? @"断开连接" : @"启动连接"; if (self.startupError.length > 0) { self.statusLabel.stringValue = [@"● 连接器未启动 · " stringByAppendingString:self.startupError]; self.statusLabel.textColor = NSColor.systemRedColor; return; } if (!self.service || !self.service->running()) { self.statusLabel.stringValue = @"● 连接器已断开"; self.statusLabel.textColor = NSColor.secondaryLabelColor; return; } const std::string spine = connector::find_spine_executable(); const BOOL showPathControls = spine.empty(); for (NSView* view in self.pathControls) view.hidden = !showPathControls; self.statusLabel.stringValue = spine.empty() ? @"● 连接器运行中 · 未找到 Spine" : @"● 连接器运行中 · Spine 已就绪"; self.statusLabel.textColor = spine.empty() ? NSColor.systemOrangeColor : NSColor.systemGreenColor; } - (void)toggleConnection:(id)sender { (void)sender; if (self.service && self.service->running()) { self.service->stop(); self.startupError = @""; } else if (self.service) { std::string error; self.service->start(error); self.startupError = [NSString stringWithUTF8String:error.c_str()]; } [self refreshStatus:nil]; } - (void)selectSpine:(id)sender { (void)sender; NSOpenPanel* panel = [NSOpenPanel openPanel]; panel.canChooseFiles = YES; panel.canChooseDirectories = NO; panel.allowsMultipleSelection = NO; panel.message = @"请选择 Spine 命令行可执行文件(macOS 通常为 Spine.app/Contents/MacOS/Spine)"; if ([panel runModal] == NSModalResponseOK) self.pathField.stringValue = panel.URL.path; } - (void)clearPath:(id)sender { (void)sender; self.pathField.stringValue = @""; [self savePath:nil]; } - (void)savePath:(id)sender { (void)sender; const std::string path = self.pathField.stringValue.UTF8String ?: ""; if (!path.empty() && !connector::valid_spine_executable(path)) { NSAlert* alert = [[NSAlert alloc] init]; alert.messageText = @"Spine 路径无效"; alert.informativeText = @"请选择实际存在的 Spine 命令行可执行文件。"; [alert runModal]; return; } try { ConnectorConfig config = connector::load_config(); config.spine_executable = path; connector::save_config(config); [self refreshStatus:nil]; } catch (const std::exception& error) { NSAlert* alert = [[NSAlert alloc] init]; alert.messageText = @"保存失败"; alert.informativeText = [NSString stringWithUTF8String:error.what()]; [alert runModal]; } } - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication*)sender { (void)sender; return YES; } - (void)applicationWillTerminate:(NSNotification*)notification { (void)notification; if (self.service) self.service->stop(); } @end int main(int argc, char* argv[]) { (void)argc; (void)argv; @autoreleasepool { ConnectorService service; std::string startup_error; service.start(startup_error); NSApplication* application = [NSApplication sharedApplication]; application.activationPolicy = NSApplicationActivationPolicyRegular; application.appearance = [NSAppearance appearanceNamed:NSAppearanceNameDarkAqua]; ConnectorAppDelegate* delegate = [[ConnectorAppDelegate alloc] init]; delegate.service = &service; delegate.startupError = [NSString stringWithUTF8String:startup_error.c_str()]; application.delegate = delegate; [application run]; service.stop(); } return 0; } #endif