185 lines
7.2 KiB
Plaintext
185 lines
7.2 KiB
Plaintext
#ifdef __APPLE__
|
||
|
||
#import <AppKit/AppKit.h>
|
||
|
||
#include "config.hpp"
|
||
#include "connector_service.hpp"
|
||
#include "platform.hpp"
|
||
|
||
using connector::ConnectorConfig;
|
||
using connector::ConnectorService;
|
||
|
||
@interface ConnectorAppDelegate : NSObject <NSApplicationDelegate, NSWindowDelegate>
|
||
@property(nonatomic, strong) NSWindow* window;
|
||
@property(nonatomic, strong) NSTextField* pathField;
|
||
@property(nonatomic, strong) NSTextField* statusLabel;
|
||
@property(nonatomic, strong) NSArray<NSView*>* 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
|