1//----------------------------------------------------------------------------- 2// Our main() function, and Cocoa-specific stuff to set up our windows and 3// otherwise handle our interface to the operating system. Everything 4// outside gtk/... should be standard C++ and OpenGL. 5// 6// Copyright 2015 <whitequark@whitequark.org> 7//----------------------------------------------------------------------------- 8#include <mach/mach.h> 9#include <mach/clock.h> 10 11#import <AppKit/AppKit.h> 12 13#include <iostream> 14#include <map> 15 16#include "solvespace.h" 17#include "../unix/gloffscreen.h" 18#include <config.h> 19 20using SolveSpace::dbp; 21 22#define GL_CHECK() \ 23 do { \ 24 int err = (int)glGetError(); \ 25 if(err) dbp("%s:%d: glGetError() == 0x%X", __FILE__, __LINE__, err); \ 26 } while (0) 27 28/* Settings */ 29 30namespace SolveSpace { 31void CnfFreezeInt(uint32_t val, const std::string &key) { 32 [[NSUserDefaults standardUserDefaults] 33 setInteger:val forKey:[NSString stringWithUTF8String:key.c_str()]]; 34} 35 36uint32_t CnfThawInt(uint32_t val, const std::string &key) { 37 NSString *nsKey = [NSString stringWithUTF8String:key.c_str()]; 38 if([[NSUserDefaults standardUserDefaults] objectForKey:nsKey]) 39 return [[NSUserDefaults standardUserDefaults] integerForKey:nsKey]; 40 return val; 41} 42 43void CnfFreezeFloat(float val, const std::string &key) { 44 [[NSUserDefaults standardUserDefaults] 45 setFloat:val forKey:[NSString stringWithUTF8String:key.c_str()]]; 46} 47 48float CnfThawFloat(float val, const std::string &key) { 49 NSString *nsKey = [NSString stringWithUTF8String:key.c_str()]; 50 if([[NSUserDefaults standardUserDefaults] objectForKey:nsKey]) 51 return [[NSUserDefaults standardUserDefaults] floatForKey:nsKey]; 52 return val; 53} 54 55void CnfFreezeString(const std::string &val, const std::string &key) { 56 [[NSUserDefaults standardUserDefaults] 57 setObject:[NSString stringWithUTF8String:val.c_str()] 58 forKey:[NSString stringWithUTF8String:key.c_str()]]; 59} 60 61std::string CnfThawString(const std::string &val, const std::string &key) { 62 NSString *nsKey = [NSString stringWithUTF8String:key.c_str()]; 63 if([[NSUserDefaults standardUserDefaults] objectForKey:nsKey]) { 64 NSString *nsNewVal = [[NSUserDefaults standardUserDefaults] stringForKey:nsKey]; 65 return [nsNewVal UTF8String]; 66 } 67 return val; 68} 69}; 70 71/* Timer */ 72 73int64_t SolveSpace::GetMilliseconds(void) { 74 clock_serv_t cclock; 75 mach_timespec_t mts; 76 77 host_get_clock_service(mach_host_self(), SYSTEM_CLOCK, &cclock); 78 clock_get_time(cclock, &mts); 79 mach_port_deallocate(mach_task_self(), cclock); 80 81 return mts.tv_sec * 1000 + mts.tv_nsec / 1000000; 82} 83 84@interface DeferredHandler : NSObject 85+ (void) runLater:(id)dummy; 86+ (void) runCallback; 87+ (void) doAutosave; 88@end 89 90@implementation DeferredHandler 91+ (void) runLater:(id)dummy { 92 SolveSpace::SS.DoLater(); 93} 94+ (void) runCallback { 95 SolveSpace::SS.GW.TimerCallback(); 96 SolveSpace::SS.TW.TimerCallback(); 97} 98+ (void) doAutosave { 99 SolveSpace::SS.Autosave(); 100} 101@end 102 103static void Schedule(SEL selector, double interval) { 104 NSMethodSignature *signature = [[DeferredHandler class] 105 methodSignatureForSelector:selector]; 106 NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; 107 [invocation setSelector:selector]; 108 [invocation setTarget:[DeferredHandler class]]; 109 [NSTimer scheduledTimerWithTimeInterval:interval 110 invocation:invocation repeats:NO]; 111} 112 113void SolveSpace::SetTimerFor(int milliseconds) { 114 Schedule(@selector(runCallback), milliseconds / 1000.0); 115} 116 117void SolveSpace::SetAutosaveTimerFor(int minutes) { 118 Schedule(@selector(doAutosave), minutes * 60.0); 119} 120 121void SolveSpace::ScheduleLater() { 122 [[NSRunLoop currentRunLoop] 123 performSelector:@selector(runLater:) 124 target:[DeferredHandler class] argument:nil 125 order:0 modes:@[NSDefaultRunLoopMode]]; 126} 127 128/* OpenGL view */ 129 130@interface GLViewWithEditor : NSView 131- (void)drawGL; 132 133@property BOOL wantsBackingStoreScaling; 134 135@property(readonly, getter=isEditing) BOOL editing; 136- (void)startEditing:(NSString*)text at:(NSPoint)origin withHeight:(double)fontHeight 137 usingMonospace:(BOOL)isMonospace; 138- (void)stopEditing; 139- (void)didEdit:(NSString*)text; 140@end 141 142@implementation GLViewWithEditor 143{ 144 GLOffscreen *offscreen; 145 NSOpenGLContext *glContext; 146@protected 147 NSTextField *editor; 148} 149 150- initWithFrame:(NSRect)frameRect { 151 self = [super initWithFrame:frameRect]; 152 [self setWantsLayer:YES]; 153 154 NSOpenGLPixelFormatAttribute attrs[] = { 155 NSOpenGLPFAColorSize, 24, 156 NSOpenGLPFADepthSize, 24, 157 0 158 }; 159 NSOpenGLPixelFormat *pixelFormat = [[NSOpenGLPixelFormat alloc] initWithAttributes:attrs]; 160 glContext = [[NSOpenGLContext alloc] initWithFormat:pixelFormat shareContext:NULL]; 161 162 editor = [[NSTextField alloc] init]; 163 [editor setEditable:YES]; 164 [[editor cell] setWraps:NO]; 165 [[editor cell] setScrollable:YES]; 166 [editor setBezeled:NO]; 167 [editor setTarget:self]; 168 [editor setAction:@selector(editorAction:)]; 169 170 return self; 171} 172 173- (void)dealloc { 174 delete offscreen; 175} 176 177#define CONVERT1(name, to_from) \ 178 - (NS##name)convert##name##to_from##Backing:(NS##name)input { \ 179 return _wantsBackingStoreScaling ? [super convert##name##to_from##Backing:input] : input; } 180#define CONVERT(name) CONVERT1(name, To) CONVERT1(name, From) 181CONVERT(Size) 182CONVERT(Rect) 183#undef CONVERT 184#undef CONVERT1 185 186- (NSPoint)convertPointToBacking:(NSPoint)input { 187 if(_wantsBackingStoreScaling) return [super convertPointToBacking:input]; 188 else { 189 input.y *= -1; 190 return input; 191 } 192} 193 194- (NSPoint)convertPointFromBacking:(NSPoint)input { 195 if(_wantsBackingStoreScaling) return [super convertPointFromBacking:input]; 196 else { 197 input.y *= -1; 198 return input; 199 } 200} 201 202- (void)drawRect:(NSRect)aRect { 203 [glContext makeCurrentContext]; 204 205 if(!offscreen) 206 offscreen = new GLOffscreen; 207 208 NSSize size = [self convertSizeToBacking:[self bounds].size]; 209 offscreen->begin(size.width, size.height); 210 211 [self drawGL]; 212 GL_CHECK(); 213 214 uint8_t *pixels = offscreen->end(![self isFlipped]); 215 CGDataProviderRef provider = CGDataProviderCreateWithData( 216 NULL, pixels, size.width * size.height * 4, NULL); 217 CGColorSpaceRef colorspace = CGColorSpaceCreateDeviceRGB(); 218 CGImageRef image = CGImageCreate(size.width, size.height, 8, 32, 219 size.width * 4, colorspace, kCGBitmapByteOrder32Little | kCGImageAlphaNoneSkipFirst, 220 provider, NULL, true, kCGRenderingIntentDefault); 221 222 CGContextDrawImage((CGContextRef) [[NSGraphicsContext currentContext] graphicsPort], 223 [self bounds], image); 224 225 CGImageRelease(image); 226 CGDataProviderRelease(provider); 227} 228 229- (void)drawGL { 230} 231 232@synthesize editing; 233 234- (void)startEditing:(NSString*)text at:(NSPoint)origin withHeight:(double)fontHeight 235 usingMonospace:(BOOL)isMonospace { 236 if(!self->editing) { 237 [self addSubview:editor]; 238 self->editing = YES; 239 } 240 241 NSFont *font; 242 if(isMonospace) 243 font = [NSFont fontWithName:@"Monaco" size:fontHeight]; 244 else 245 font = [NSFont controlContentFontOfSize:fontHeight]; 246 [editor setFont:font]; 247 248 origin.x -= 3; /* left padding; no way to get it from NSTextField */ 249 origin.y -= [editor intrinsicContentSize].height; 250 origin.y += [editor baselineOffsetFromBottom]; 251 252 [editor setFrameOrigin:origin]; 253 [editor setStringValue:text]; 254 [[self window] makeFirstResponder:editor]; 255} 256 257- (void)stopEditing { 258 if(self->editing) { 259 [editor removeFromSuperview]; 260 self->editing = NO; 261 } 262} 263 264- (void)editorAction:(id)sender { 265 [self didEdit:[editor stringValue]]; 266 [self stopEditing]; 267} 268 269- (void)didEdit:(NSString*)text { 270} 271@end 272 273/* Graphics window */ 274 275@interface GraphicsWindowView : GLViewWithEditor 276{ 277 NSTrackingArea *trackingArea; 278} 279 280@property(readonly) NSEvent *lastContextMenuEvent; 281@end 282 283@implementation GraphicsWindowView 284- (BOOL)isFlipped { 285 return YES; 286} 287 288- (void)drawGL { 289 SolveSpace::SS.GW.Paint(); 290} 291 292- (BOOL)acceptsFirstResponder { 293 return YES; 294} 295 296- (void) createTrackingArea { 297 trackingArea = [[NSTrackingArea alloc] initWithRect:[self bounds] 298 options:(NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | 299 NSTrackingActiveInKeyWindow) 300 owner:self userInfo:nil]; 301 [self addTrackingArea:trackingArea]; 302} 303 304- (void) updateTrackingAreas 305{ 306 [self removeTrackingArea:trackingArea]; 307 [self createTrackingArea]; 308 [super updateTrackingAreas]; 309} 310 311- (void)mouseMoved:(NSEvent*)event { 312 NSPoint point = [self ij_to_xy:[self convertPoint:[event locationInWindow] fromView:nil]]; 313 NSUInteger flags = [event modifierFlags]; 314 NSUInteger buttons = [NSEvent pressedMouseButtons]; 315 SolveSpace::SS.GW.MouseMoved(point.x, point.y, 316 buttons & (1 << 0), 317 buttons & (1 << 2), 318 buttons & (1 << 1), 319 flags & NSShiftKeyMask, 320 flags & NSCommandKeyMask); 321} 322 323- (void)mouseDragged:(NSEvent*)event { 324 [self mouseMoved:event]; 325} 326 327- (void)rightMouseDragged:(NSEvent*)event { 328 [self mouseMoved:event]; 329} 330 331- (void)otherMouseDragged:(NSEvent*)event { 332 [self mouseMoved:event]; 333} 334 335- (void)mouseDown:(NSEvent*)event { 336 NSPoint point = [self ij_to_xy:[self convertPoint:[event locationInWindow] fromView:nil]]; 337 if([event clickCount] == 1) 338 SolveSpace::SS.GW.MouseLeftDown(point.x, point.y); 339 else if([event clickCount] == 2) 340 SolveSpace::SS.GW.MouseLeftDoubleClick(point.x, point.y); 341} 342 343- (void)rightMouseDown:(NSEvent*)event { 344 NSPoint point = [self ij_to_xy:[self convertPoint:[event locationInWindow] fromView:nil]]; 345 SolveSpace::SS.GW.MouseMiddleOrRightDown(point.x, point.y); 346} 347 348- (void)otherMouseDown:(NSEvent*)event { 349 [self rightMouseDown:event]; 350} 351 352- (void)mouseUp:(NSEvent*)event { 353 NSPoint point = [self ij_to_xy:[self convertPoint:[event locationInWindow] fromView:nil]]; 354 SolveSpace::SS.GW.MouseLeftUp(point.x, point.y); 355} 356 357- (void)rightMouseUp:(NSEvent*)event { 358 NSPoint point = [self ij_to_xy:[self convertPoint:[event locationInWindow] fromView:nil]]; 359 self->_lastContextMenuEvent = event; 360 SolveSpace::SS.GW.MouseRightUp(point.x, point.y); 361} 362 363- (void)scrollWheel:(NSEvent*)event { 364 NSPoint point = [self ij_to_xy:[self convertPoint:[event locationInWindow] fromView:nil]]; 365 SolveSpace::SS.GW.MouseScroll(point.x, point.y, -[event deltaY]); 366} 367 368- (void)mouseExited:(NSEvent*)event { 369 SolveSpace::SS.GW.MouseLeave(); 370} 371 372- (void)keyDown:(NSEvent*)event { 373 int chr = 0; 374 if(NSString *nsChr = [event charactersIgnoringModifiers]) 375 chr = [nsChr characterAtIndex:0]; 376 377 if(chr >= NSF1FunctionKey && chr <= NSF12FunctionKey) 378 chr = SolveSpace::GraphicsWindow::FUNCTION_KEY_BASE + (chr - NSF1FunctionKey); 379 380 NSUInteger flags = [event modifierFlags]; 381 if(flags & NSShiftKeyMask) 382 chr |= SolveSpace::GraphicsWindow::SHIFT_MASK; 383 if(flags & NSCommandKeyMask) 384 chr |= SolveSpace::GraphicsWindow::CTRL_MASK; 385 386 // override builtin behavior: "focus on next cell", "close window" 387 if(chr == '\t' || chr == '\x1b') 388 [[NSApp mainMenu] performKeyEquivalent:event]; 389 else if(!chr || !SolveSpace::SS.GW.KeyDown(chr)) 390 [super keyDown:event]; 391} 392 393- (void)startEditing:(NSString*)text at:(NSPoint)xy withHeight:(double)fontHeight 394 withMinWidthInChars:(int)minWidthChars { 395 // Convert to ij (vs. xy) style coordinates 396 NSSize size = [self convertSizeToBacking:[self bounds].size]; 397 NSPoint point = { 398 .x = xy.x + size.width / 2, 399 .y = xy.y - size.height / 2 400 }; 401 [[self window] makeKeyWindow]; 402 [super startEditing:text at:[self convertPointFromBacking:point] 403 withHeight:fontHeight usingMonospace:FALSE]; 404 [self prepareEditorWithMinWidthInChars:minWidthChars]; 405} 406 407- (void)prepareEditorWithMinWidthInChars:(int)minWidthChars { 408 NSFont *font = [editor font]; 409 NSGlyph glyphA = [font glyphWithName:@"a"]; 410 if(glyphA == -1) oops(); 411 CGFloat glyphAWidth = [font advancementForGlyph:glyphA].width; 412 413 [editor sizeToFit]; 414 415 NSSize frameSize = [editor frame].size; 416 frameSize.width = std::max(frameSize.width, glyphAWidth * minWidthChars); 417 [editor setFrameSize:frameSize]; 418} 419 420- (void)didEdit:(NSString*)text { 421 SolveSpace::SS.GW.EditControlDone([text UTF8String]); 422 [self setNeedsDisplay:YES]; 423} 424 425- (void)cancelOperation:(id)sender { 426 [self stopEditing]; 427} 428 429- (NSPoint)ij_to_xy:(NSPoint)ij { 430 // Convert to xy (vs. ij) style coordinates, 431 // with (0, 0) at center 432 NSSize size = [self bounds].size; 433 return [self convertPointToBacking:(NSPoint){ 434 .x = ij.x - size.width / 2, .y = ij.y - size.height / 2 }]; 435} 436@end 437 438@interface GraphicsWindowDelegate : NSObject<NSWindowDelegate> 439- (BOOL)windowShouldClose:(id)sender; 440 441@property(readonly, getter=isFullscreen) BOOL fullscreen; 442- (void)windowDidEnterFullScreen:(NSNotification *)notification; 443- (void)windowDidExitFullScreen:(NSNotification *)notification; 444@end 445 446@implementation GraphicsWindowDelegate 447- (BOOL)windowShouldClose:(id)sender { 448 [NSApp terminate:sender]; 449 return FALSE; /* in case NSApp changes its mind */ 450} 451 452@synthesize fullscreen; 453- (void)windowDidEnterFullScreen:(NSNotification *)notification { 454 fullscreen = true; 455 /* Update the menus */ 456 SolveSpace::SS.GW.EnsureValidActives(); 457} 458- (void)windowDidExitFullScreen:(NSNotification *)notification { 459 fullscreen = false; 460 /* Update the menus */ 461 SolveSpace::SS.GW.EnsureValidActives(); 462} 463@end 464 465static NSWindow *GW; 466static GraphicsWindowView *GWView; 467static GraphicsWindowDelegate *GWDelegate; 468 469namespace SolveSpace { 470void InitGraphicsWindow() { 471 GW = [[NSWindow alloc] init]; 472 GWDelegate = [[GraphicsWindowDelegate alloc] init]; 473 [GW setDelegate:GWDelegate]; 474 [GW setStyleMask:(NSTitledWindowMask | NSClosableWindowMask | 475 NSMiniaturizableWindowMask | NSResizableWindowMask)]; 476 [GW setFrameAutosaveName:@"GraphicsWindow"]; 477 [GW setCollectionBehavior:NSWindowCollectionBehaviorFullScreenPrimary]; 478 if(![GW setFrameUsingName:[GW frameAutosaveName]]) 479 [GW setContentSize:(NSSize){ .width = 600, .height = 600 }]; 480 GWView = [[GraphicsWindowView alloc] init]; 481 [GW setContentView:GWView]; 482} 483 484void GetGraphicsWindowSize(int *w, int *h) { 485 NSSize size = [GWView convertSizeToBacking:[GWView frame].size]; 486 *w = size.width; 487 *h = size.height; 488} 489 490void InvalidateGraphics(void) { 491 [GWView setNeedsDisplay:YES]; 492} 493 494void PaintGraphics(void) { 495 [GWView setNeedsDisplay:YES]; 496 CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, YES); 497} 498 499void SetCurrentFilename(const std::string &filename) { 500 if(!filename.empty()) { 501 [GW setTitleWithRepresentedFilename:[NSString stringWithUTF8String:filename.c_str()]]; 502 } else { 503 [GW setTitle:@"(new sketch)"]; 504 [GW setRepresentedFilename:@""]; 505 } 506} 507 508void ToggleFullScreen(void) { 509 [GW toggleFullScreen:nil]; 510} 511 512bool FullScreenIsActive(void) { 513 return [GWDelegate isFullscreen]; 514} 515 516void ShowGraphicsEditControl(int x, int y, int fontHeight, int minWidthChars, 517 const std::string &str) { 518 [GWView startEditing:[NSString stringWithUTF8String:str.c_str()] 519 at:(NSPoint){(CGFloat)x, (CGFloat)y} 520 withHeight:fontHeight 521 withMinWidthInChars:minWidthChars]; 522} 523 524void HideGraphicsEditControl(void) { 525 [GWView stopEditing]; 526} 527 528bool GraphicsEditControlIsVisible(void) { 529 return [GWView isEditing]; 530} 531} 532 533/* Context menus */ 534 535static int contextMenuChoice; 536 537@interface ContextMenuResponder : NSObject 538+ (void)handleClick:(id)sender; 539@end 540 541@implementation ContextMenuResponder 542+ (void)handleClick:(id)sender { 543 contextMenuChoice = [sender tag]; 544} 545@end 546 547namespace SolveSpace { 548NSMenu *contextMenu, *contextSubmenu; 549 550void AddContextMenuItem(const char *label, int id_) { 551 NSMenuItem *menuItem; 552 if(label) { 553 menuItem = [[NSMenuItem alloc] initWithTitle:[NSString stringWithUTF8String:label] 554 action:@selector(handleClick:) keyEquivalent:@""]; 555 [menuItem setTarget:[ContextMenuResponder class]]; 556 [menuItem setTag:id_]; 557 } else { 558 menuItem = [NSMenuItem separatorItem]; 559 } 560 561 if(id_ == CONTEXT_SUBMENU) { 562 [menuItem setSubmenu:contextSubmenu]; 563 contextSubmenu = nil; 564 } 565 566 if(contextSubmenu) { 567 [contextSubmenu addItem:menuItem]; 568 } else { 569 if(!contextMenu) { 570 contextMenu = [[NSMenu alloc] 571 initWithTitle:[NSString stringWithUTF8String:label]]; 572 } 573 574 [contextMenu addItem:menuItem]; 575 } 576} 577 578void CreateContextSubmenu(void) { 579 if(contextSubmenu) oops(); 580 581 contextSubmenu = [[NSMenu alloc] initWithTitle:@""]; 582} 583 584int ShowContextMenu(void) { 585 if(!contextMenu) 586 return -1; 587 588 [NSMenu popUpContextMenu:contextMenu 589 withEvent:[GWView lastContextMenuEvent] forView:GWView]; 590 591 contextMenu = nil; 592 593 return contextMenuChoice; 594} 595}; 596 597/* Main menu */ 598 599@interface MainMenuResponder : NSObject 600+ (void)handleStatic:(id)sender; 601+ (void)handleRecent:(id)sender; 602@end 603 604@implementation MainMenuResponder 605+ (void)handleStatic:(id)sender { 606 SolveSpace::GraphicsWindow::MenuEntry *entry = 607 (SolveSpace::GraphicsWindow::MenuEntry*)[sender tag]; 608 609 if(entry->fn && ![(NSMenuItem*)sender hasSubmenu]) 610 entry->fn(entry->id); 611} 612 613+ (void)handleRecent:(id)sender { 614 int id_ = [sender tag]; 615 if(id_ >= RECENT_OPEN && id_ < (RECENT_OPEN + MAX_RECENT)) 616 SolveSpace::SolveSpaceUI::MenuFile(id_); 617 else if(id_ >= RECENT_LINK && id_ < (RECENT_LINK + MAX_RECENT)) 618 SolveSpace::Group::MenuGroup(id_); 619} 620@end 621 622namespace SolveSpace { 623std::map<int, NSMenuItem*> mainMenuItems; 624 625void InitMainMenu(NSMenu *mainMenu) { 626 NSMenuItem *menuItem = NULL; 627 NSMenu *levels[5] = {mainMenu, 0}; 628 NSString *label; 629 630 const GraphicsWindow::MenuEntry *entry = &GraphicsWindow::menu[0]; 631 int current_level = 0; 632 while(entry->level >= 0) { 633 if(entry->level > current_level) { 634 NSMenu *menu = [[NSMenu alloc] initWithTitle:label]; 635 [menu setAutoenablesItems:NO]; 636 [menuItem setSubmenu:menu]; 637 638 if(entry->level >= sizeof(levels) / sizeof(levels[0])) 639 oops(); 640 641 levels[entry->level] = menu; 642 } 643 644 current_level = entry->level; 645 646 if(entry->label) { 647 /* OS X does not support mnemonics */ 648 label = [[NSString stringWithUTF8String:entry->label] 649 stringByReplacingOccurrencesOfString:@"&" withString:@""]; 650 651 unichar accelChar = entry->accel & 652 ~(GraphicsWindow::SHIFT_MASK | GraphicsWindow::CTRL_MASK); 653 if(accelChar > GraphicsWindow::FUNCTION_KEY_BASE && 654 accelChar <= GraphicsWindow::FUNCTION_KEY_BASE + 12) { 655 accelChar = NSF1FunctionKey + (accelChar - GraphicsWindow::FUNCTION_KEY_BASE - 1); 656 } else if(accelChar == GraphicsWindow::DELETE_KEY) { 657 accelChar = NSBackspaceCharacter; 658 } 659 NSString *accel = [NSString stringWithCharacters:&accelChar length:1]; 660 661 menuItem = [levels[entry->level] addItemWithTitle:label 662 action:NULL keyEquivalent:[accel lowercaseString]]; 663 664 NSUInteger modifierMask = 0; 665 if(entry->accel & GraphicsWindow::SHIFT_MASK) 666 modifierMask |= NSShiftKeyMask; 667 else if(entry->accel & GraphicsWindow::CTRL_MASK) 668 modifierMask |= NSCommandKeyMask; 669 [menuItem setKeyEquivalentModifierMask:modifierMask]; 670 671 [menuItem setTag:(NSInteger)entry]; 672 [menuItem setTarget:[MainMenuResponder class]]; 673 [menuItem setAction:@selector(handleStatic:)]; 674 } else { 675 [levels[entry->level] addItem:[NSMenuItem separatorItem]]; 676 } 677 678 mainMenuItems[entry->id] = menuItem; 679 680 ++entry; 681 } 682} 683 684void EnableMenuById(int id_, bool enabled) { 685 [mainMenuItems[id_] setEnabled:enabled]; 686} 687 688void CheckMenuById(int id_, bool checked) { 689 [mainMenuItems[id_] setState:(checked ? NSOnState : NSOffState)]; 690} 691 692void RadioMenuById(int id_, bool selected) { 693 CheckMenuById(id_, selected); 694} 695 696static void RefreshRecentMenu(int id_, int base) { 697 NSMenuItem *recent = mainMenuItems[id_]; 698 NSMenu *menu = [[NSMenu alloc] initWithTitle:@""]; 699 [recent setSubmenu:menu]; 700 701 if(std::string(RecentFile[0]).empty()) { 702 NSMenuItem *placeholder = [[NSMenuItem alloc] 703 initWithTitle:@"(no recent files)" action:nil keyEquivalent:@""]; 704 [placeholder setEnabled:NO]; 705 [menu addItem:placeholder]; 706 } else { 707 for(int i = 0; i < MAX_RECENT; i++) { 708 if(std::string(RecentFile[i]).empty()) 709 break; 710 711 NSMenuItem *item = [[NSMenuItem alloc] 712 initWithTitle:[[NSString stringWithUTF8String:RecentFile[i].c_str()] 713 stringByAbbreviatingWithTildeInPath] 714 action:nil keyEquivalent:@""]; 715 [item setTag:(base + i)]; 716 [item setAction:@selector(handleRecent:)]; 717 [item setTarget:[MainMenuResponder class]]; 718 [menu addItem:item]; 719 } 720 } 721} 722 723void RefreshRecentMenus(void) { 724 RefreshRecentMenu(GraphicsWindow::MNU_OPEN_RECENT, RECENT_OPEN); 725 RefreshRecentMenu(GraphicsWindow::MNU_GROUP_RECENT, RECENT_LINK); 726} 727 728void ToggleMenuBar(void) { 729 [NSMenu setMenuBarVisible:![NSMenu menuBarVisible]]; 730} 731 732bool MenuBarIsVisible(void) { 733 return [NSMenu menuBarVisible]; 734} 735} 736 737/* Save/load */ 738 739bool SolveSpace::GetOpenFile(std::string *file, const std::string &defExtension, 740 const FileFilter ssFilters[]) { 741 NSOpenPanel *panel = [NSOpenPanel openPanel]; 742 NSMutableArray *filters = [[NSMutableArray alloc] init]; 743 for(const FileFilter *ssFilter = ssFilters; ssFilter->name; ssFilter++) { 744 for(const char *const *ssPattern = ssFilter->patterns; *ssPattern; ssPattern++) { 745 [filters addObject:[NSString stringWithUTF8String:*ssPattern]]; 746 } 747 } 748 [filters removeObjectIdenticalTo:@"*"]; 749 [panel setAllowedFileTypes:filters]; 750 751 if([panel runModal] == NSFileHandlingPanelOKButton) { 752 *file = [[NSFileManager defaultManager] 753 fileSystemRepresentationWithPath:[[panel URL] path]]; 754 return true; 755 } else { 756 return false; 757 } 758} 759 760@interface SaveFormatController : NSViewController 761@property NSSavePanel *panel; 762@property NSArray *extensions; 763@property (nonatomic) IBOutlet NSPopUpButton *button; 764@property (nonatomic) NSInteger index; 765@end 766 767@implementation SaveFormatController 768@synthesize panel, extensions, button, index; 769- (void)setIndex:(NSInteger)newIndex { 770 self->index = newIndex; 771 NSString *extension = [extensions objectAtIndex:newIndex]; 772 if(![extension isEqual:@"*"]) { 773 NSString *filename = [panel nameFieldStringValue]; 774 NSString *basename = [[filename componentsSeparatedByString:@"."] objectAtIndex:0]; 775 [panel setNameFieldStringValue:[basename stringByAppendingPathExtension:extension]]; 776 } 777} 778@end 779 780bool SolveSpace::GetSaveFile(std::string *file, const std::string &defExtension, 781 const FileFilter ssFilters[]) { 782 NSSavePanel *panel = [NSSavePanel savePanel]; 783 784 SaveFormatController *controller = 785 [[SaveFormatController alloc] initWithNibName:@"SaveFormatAccessory" bundle:nil]; 786 [controller setPanel:panel]; 787 [panel setAccessoryView:[controller view]]; 788 789 NSMutableArray *extensions = [[NSMutableArray alloc] init]; 790 [controller setExtensions:extensions]; 791 792 NSPopUpButton *button = [controller button]; 793 [button removeAllItems]; 794 for(const FileFilter *ssFilter = ssFilters; ssFilter->name; ssFilter++) { 795 std::string desc; 796 for(const char *const *ssPattern = ssFilter->patterns; *ssPattern; ssPattern++) { 797 if(desc == "") { 798 desc = *ssPattern; 799 } else { 800 desc += ", "; 801 desc += *ssPattern; 802 } 803 } 804 std::string title = std::string(ssFilter->name) + " (" + desc + ")"; 805 [button addItemWithTitle:[NSString stringWithUTF8String:title.c_str()]]; 806 [extensions addObject:[NSString stringWithUTF8String:ssFilter->patterns[0]]]; 807 } 808 809 int extensionIndex = 0; 810 if(defExtension != "") { 811 extensionIndex = [extensions indexOfObject: 812 [NSString stringWithUTF8String:defExtension.c_str()]]; 813 if(extensionIndex == -1) { 814 extensionIndex = 0; 815 } 816 } 817 818 [button selectItemAtIndex:extensionIndex]; 819 [panel setNameFieldStringValue:[@"untitled" 820 stringByAppendingPathExtension:[extensions objectAtIndex:extensionIndex]]]; 821 822 if([panel runModal] == NSFileHandlingPanelOKButton) { 823 *file = [[NSFileManager defaultManager] 824 fileSystemRepresentationWithPath:[[panel URL] path]]; 825 return true; 826 } else { 827 return false; 828 } 829} 830 831SolveSpace::DialogChoice SolveSpace::SaveFileYesNoCancel(void) { 832 NSAlert *alert = [[NSAlert alloc] init]; 833 if(!std::string(SolveSpace::SS.saveFile).empty()) { 834 [alert setMessageText: 835 [[@"Do you want to save the changes you made to the sketch “" 836 stringByAppendingString: 837 [[NSString stringWithUTF8String:SolveSpace::SS.saveFile.c_str()] 838 stringByAbbreviatingWithTildeInPath]] 839 stringByAppendingString:@"”?"]]; 840 } else { 841 [alert setMessageText:@"Do you want to save the changes you made to the new sketch?"]; 842 } 843 [alert setInformativeText:@"Your changes will be lost if you don't save them."]; 844 [alert addButtonWithTitle:@"Save"]; 845 [alert addButtonWithTitle:@"Cancel"]; 846 [alert addButtonWithTitle:@"Don't Save"]; 847 switch([alert runModal]) { 848 case NSAlertFirstButtonReturn: 849 return DIALOG_YES; 850 case NSAlertSecondButtonReturn: 851 default: 852 return DIALOG_CANCEL; 853 case NSAlertThirdButtonReturn: 854 return DIALOG_NO; 855 } 856} 857 858SolveSpace::DialogChoice SolveSpace::LoadAutosaveYesNo(void) { 859 NSAlert *alert = [[NSAlert alloc] init]; 860 [alert setMessageText: 861 @"An autosave file is availible for this project."]; 862 [alert setInformativeText: 863 @"Do you want to load the autosave file instead?"]; 864 [alert addButtonWithTitle:@"Load"]; 865 [alert addButtonWithTitle:@"Don't Load"]; 866 switch([alert runModal]) { 867 case NSAlertFirstButtonReturn: 868 return DIALOG_YES; 869 case NSAlertSecondButtonReturn: 870 default: 871 return DIALOG_NO; 872 } 873} 874 875SolveSpace::DialogChoice SolveSpace::LocateImportedFileYesNoCancel( 876 const std::string &filename, bool canCancel) { 877 NSAlert *alert = [[NSAlert alloc] init]; 878 [alert setMessageText:[NSString stringWithUTF8String: 879 ("The linked file " + filename + " is not present.").c_str()]]; 880 [alert setInformativeText: 881 @"Do you want to locate it manually?\n" 882 "If you select \"No\", any geometry that depends on " 883 "the missing file will be removed."]; 884 [alert addButtonWithTitle:@"Yes"]; 885 if(canCancel) 886 [alert addButtonWithTitle:@"Cancel"]; 887 [alert addButtonWithTitle:@"No"]; 888 switch([alert runModal]) { 889 case NSAlertFirstButtonReturn: 890 return DIALOG_YES; 891 case NSAlertSecondButtonReturn: 892 default: 893 if(canCancel) 894 return DIALOG_CANCEL; 895 /* fallthrough */ 896 case NSAlertThirdButtonReturn: 897 return DIALOG_NO; 898 } 899} 900 901/* Text window */ 902 903@interface TextWindowView : GLViewWithEditor 904{ 905 NSTrackingArea *trackingArea; 906} 907 908@property (nonatomic, getter=isCursorHand) BOOL cursorHand; 909@end 910 911@implementation TextWindowView 912- (BOOL)isFlipped { 913 return YES; 914} 915 916- (void)drawGL { 917 SolveSpace::SS.TW.Paint(); 918} 919 920- (BOOL)acceptsFirstMouse:(NSEvent*)event { 921 return YES; 922} 923 924- (void) createTrackingArea { 925 trackingArea = [[NSTrackingArea alloc] initWithRect:[self bounds] 926 options:(NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | 927 NSTrackingActiveAlways) 928 owner:self userInfo:nil]; 929 [self addTrackingArea:trackingArea]; 930} 931 932- (void) updateTrackingAreas 933{ 934 [self removeTrackingArea:trackingArea]; 935 [self createTrackingArea]; 936 [super updateTrackingAreas]; 937} 938 939- (void)mouseMoved:(NSEvent*)event { 940 NSPoint point = [self convertPointToBacking: 941 [self convertPoint:[event locationInWindow] fromView:nil]]; 942 SolveSpace::SS.TW.MouseEvent(/*leftClick*/ false, /*leftDown*/ false, 943 point.x, -point.y); 944} 945 946- (void)mouseDown:(NSEvent*)event { 947 NSPoint point = [self convertPointToBacking: 948 [self convertPoint:[event locationInWindow] fromView:nil]]; 949 SolveSpace::SS.TW.MouseEvent(/*leftClick*/ true, /*leftDown*/ true, 950 point.x, -point.y); 951} 952 953- (void)mouseDragged:(NSEvent*)event { 954 NSPoint point = [self convertPointToBacking: 955 [self convertPoint:[event locationInWindow] fromView:nil]]; 956 SolveSpace::SS.TW.MouseEvent(/*leftClick*/ false, /*leftDown*/ true, 957 point.x, -point.y); 958} 959 960- (void)setCursorHand:(BOOL)cursorHand { 961 if(_cursorHand != cursorHand) { 962 if(cursorHand) 963 [[NSCursor pointingHandCursor] push]; 964 else 965 [NSCursor pop]; 966 } 967 _cursorHand = cursorHand; 968} 969 970- (void)mouseExited:(NSEvent*)event { 971 [self setCursorHand:FALSE]; 972 SolveSpace::SS.TW.MouseLeave(); 973} 974 975- (void)startEditing:(NSString*)text at:(NSPoint)point { 976 point = [self convertPointFromBacking:point]; 977 point.y = -point.y + 2; 978 [[self window] makeKeyWindow]; 979 [super startEditing:text at:point withHeight:15.0 usingMonospace:TRUE]; 980 [editor setFrameSize:(NSSize){ 981 .width = [self bounds].size.width - [editor frame].origin.x, 982 .height = [editor intrinsicContentSize].height }]; 983} 984 985- (void)stopEditing { 986 [super stopEditing]; 987 [GW makeKeyWindow]; 988} 989 990- (void)didEdit:(NSString*)text { 991 SolveSpace::SS.TW.EditControlDone([text UTF8String]); 992} 993 994- (void)cancelOperation:(id)sender { 995 [self stopEditing]; 996} 997@end 998 999@interface TextWindowDelegate : NSObject<NSWindowDelegate> 1000- (BOOL)windowShouldClose:(id)sender; 1001- (void)windowDidResize:(NSNotification *)notification; 1002@end 1003 1004@implementation TextWindowDelegate 1005- (BOOL)windowShouldClose:(id)sender { 1006 SolveSpace::GraphicsWindow::MenuView(SolveSpace::GraphicsWindow::MNU_SHOW_TEXT_WND); 1007 return NO; 1008} 1009 1010- (void)windowDidResize:(NSNotification *)notification { 1011 NSClipView *view = [[[notification object] contentView] contentView]; 1012 NSView *document = [view documentView]; 1013 NSSize size = [document frame].size; 1014 size.width = [view frame].size.width; 1015 [document setFrameSize:size]; 1016} 1017@end 1018 1019static NSPanel *TW; 1020static TextWindowView *TWView; 1021static TextWindowDelegate *TWDelegate; 1022 1023namespace SolveSpace { 1024void InitTextWindow() { 1025 TW = [[NSPanel alloc] init]; 1026 TWDelegate = [[TextWindowDelegate alloc] init]; 1027 [TW setStyleMask:(NSTitledWindowMask | NSClosableWindowMask | NSResizableWindowMask | 1028 NSUtilityWindowMask)]; 1029 [[TW standardWindowButton:NSWindowMiniaturizeButton] setHidden:YES]; 1030 [[TW standardWindowButton:NSWindowZoomButton] setHidden:YES]; 1031 [TW setTitle:@"Property Browser"]; 1032 [TW setFrameAutosaveName:@"TextWindow"]; 1033 [TW setFloatingPanel:YES]; 1034 [TW setBecomesKeyOnlyIfNeeded:YES]; 1035 1036 NSScrollView *scrollView = [[NSScrollView alloc] init]; 1037 [TW setContentView:scrollView]; 1038 [scrollView setBackgroundColor:[NSColor blackColor]]; 1039 [scrollView setHasVerticalScroller:YES]; 1040 [scrollView setScrollerKnobStyle:NSScrollerKnobStyleLight]; 1041 [[scrollView contentView] setCopiesOnScroll:YES]; 1042 1043 TWView = [[TextWindowView alloc] init]; 1044 [scrollView setDocumentView:TWView]; 1045 1046 [TW setDelegate:TWDelegate]; 1047 if(![TW setFrameUsingName:[TW frameAutosaveName]]) 1048 [TW setContentSize:(NSSize){ .width = 420, .height = 300 }]; 1049 [TWView setFrame:[[scrollView contentView] frame]]; 1050} 1051 1052void ShowTextWindow(bool visible) { 1053 if(visible) 1054 [TW orderFront:nil]; 1055 else 1056 [TW close]; 1057} 1058 1059void GetTextWindowSize(int *w, int *h) { 1060 NSSize size = [TWView convertSizeToBacking:[TWView frame].size]; 1061 *w = size.width; 1062 *h = size.height; 1063} 1064 1065void InvalidateText(void) { 1066 NSSize size = [TWView convertSizeToBacking:[TWView frame].size]; 1067 size.height = (SS.TW.top[SS.TW.rows - 1] + 1) * TextWindow::LINE_HEIGHT / 2; 1068 [TWView setFrameSize:[TWView convertSizeFromBacking:size]]; 1069 [TWView setNeedsDisplay:YES]; 1070} 1071 1072void MoveTextScrollbarTo(int pos, int maxPos, int page) { 1073 /* unused; we draw the entire text window and scroll in Cocoa */ 1074} 1075 1076void SetMousePointerToHand(bool is_hand) { 1077 [TWView setCursorHand:is_hand]; 1078} 1079 1080void ShowTextEditControl(int x, int y, const std::string &str) { 1081 return [TWView startEditing:[NSString stringWithUTF8String:str.c_str()] 1082 at:(NSPoint){(CGFloat)x, (CGFloat)y}]; 1083} 1084 1085void HideTextEditControl(void) { 1086 return [TWView stopEditing]; 1087} 1088 1089bool TextEditControlIsVisible(void) { 1090 return [TWView isEditing]; 1091} 1092}; 1093 1094/* Miscellanea */ 1095 1096void SolveSpace::DoMessageBox(const char *str, int rows, int cols, bool error) { 1097 NSAlert *alert = [[NSAlert alloc] init]; 1098 [alert setAlertStyle:(error ? NSWarningAlertStyle : NSInformationalAlertStyle)]; 1099 [alert addButtonWithTitle:@"OK"]; 1100 1101 /* do some additional formatting of the message these are 1102 heuristics, but they are made failsafe and lead to nice results. */ 1103 NSString *input = [NSString stringWithUTF8String:str]; 1104 NSRange dot = [input rangeOfCharacterFromSet: 1105 [NSCharacterSet characterSetWithCharactersInString:@".:"]]; 1106 if(dot.location != NSNotFound) { 1107 [alert setMessageText:[[input substringToIndex:dot.location + 1] 1108 stringByReplacingOccurrencesOfString:@"\n" withString:@" "]]; 1109 [alert setInformativeText: 1110 [[input substringFromIndex:dot.location + 1] 1111 stringByTrimmingCharactersInSet:[NSCharacterSet newlineCharacterSet]]]; 1112 } else { 1113 [alert setMessageText:[input 1114 stringByReplacingOccurrencesOfString:@"\n" withString:@" "]]; 1115 } 1116 1117 [alert runModal]; 1118} 1119 1120void SolveSpace::OpenWebsite(const char *url) { 1121 [[NSWorkspace sharedWorkspace] openURL: 1122 [NSURL URLWithString:[NSString stringWithUTF8String:url]]]; 1123} 1124 1125std::vector<std::string> SolveSpace::GetFontFiles() { 1126 std::vector<std::string> fonts; 1127 1128 NSArray *fontNames = [[NSFontManager sharedFontManager] availableFonts]; 1129 for(NSString *fontName in fontNames) { 1130 CTFontDescriptorRef fontRef = 1131 CTFontDescriptorCreateWithNameAndSize ((__bridge CFStringRef)fontName, 10.0); 1132 CFURLRef url = (CFURLRef)CTFontDescriptorCopyAttribute(fontRef, kCTFontURLAttribute); 1133 NSString *fontPath = [NSString stringWithString:[(NSURL *)CFBridgingRelease(url) path]]; 1134 fonts.push_back([[NSFileManager defaultManager] 1135 fileSystemRepresentationWithPath:fontPath]); 1136 } 1137 1138 return fonts; 1139} 1140 1141/* Application lifecycle */ 1142 1143@interface ApplicationDelegate : NSObject<NSApplicationDelegate> 1144- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)theApplication; 1145- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender; 1146- (void)applicationWillTerminate:(NSNotification *)aNotification; 1147- (BOOL)application:(NSApplication *)theApplication openFile:(NSString *)filename; 1148- (IBAction)preferences:(id)sender; 1149@end 1150 1151@implementation ApplicationDelegate 1152- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)theApplication { 1153 return YES; 1154} 1155 1156- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender { 1157 if(SolveSpace::SS.OkayToStartNewFile()) 1158 return NSTerminateNow; 1159 else 1160 return NSTerminateCancel; 1161} 1162 1163- (void)applicationWillTerminate:(NSNotification *)aNotification { 1164 SolveSpace::SS.Exit(); 1165} 1166 1167- (BOOL)application:(NSApplication *)theApplication openFile:(NSString *)filename { 1168 return SolveSpace::SS.OpenFile([filename UTF8String]); 1169} 1170 1171- (IBAction)preferences:(id)sender { 1172 SolveSpace::SS.TW.GoToScreen(SolveSpace::TextWindow::SCREEN_CONFIGURATION); 1173 SolveSpace::SS.ScheduleShowTW(); 1174} 1175@end 1176 1177void SolveSpace::ExitNow(void) { 1178 [NSApp stop:nil]; 1179} 1180 1181/* 1182 * Normally we would just link to the 3DconnexionClient framework. 1183 * We don't want to (are not allowed to) distribute the official 1184 * framework, so we're trying to use the one installed on the users 1185 * computer. There are some different versions of the framework, 1186 * the official one and re-implementations using an open source driver 1187 * for older devices (spacenav-plus). So weak-linking isn't an option, 1188 * either. The only remaining way is using CFBundle to dynamically 1189 * load the library at runtime, and also detect its availability. 1190 * 1191 * We're also defining everything needed from the 3DconnexionClientAPI, 1192 * so we're not depending on the API headers. 1193 */ 1194 1195#pragma pack(push,2) 1196 1197enum { 1198 kConnexionClientModeTakeOver = 1, 1199 kConnexionClientModePlugin = 2 1200}; 1201 1202#define kConnexionMsgDeviceState '3dSR' 1203#define kConnexionMaskButtons 0x00FF 1204#define kConnexionMaskAxis 0x3F00 1205 1206typedef struct { 1207 uint16_t version; 1208 uint16_t client; 1209 uint16_t command; 1210 int16_t param; 1211 int32_t value; 1212 UInt64 time; 1213 uint8_t report[8]; 1214 uint16_t buttons8; 1215 int16_t axis[6]; 1216 uint16_t address; 1217 uint32_t buttons; 1218} ConnexionDeviceState, *ConnexionDeviceStatePtr; 1219 1220#pragma pack(pop) 1221 1222typedef void (*ConnexionAddedHandlerProc)(io_connect_t); 1223typedef void (*ConnexionRemovedHandlerProc)(io_connect_t); 1224typedef void (*ConnexionMessageHandlerProc)(io_connect_t, natural_t, void *); 1225 1226typedef OSErr (*InstallConnexionHandlersProc)(ConnexionMessageHandlerProc, ConnexionAddedHandlerProc, ConnexionRemovedHandlerProc); 1227typedef void (*CleanupConnexionHandlersProc)(void); 1228typedef UInt16 (*RegisterConnexionClientProc)(UInt32, UInt8 *, UInt16, UInt32); 1229typedef void (*UnregisterConnexionClientProc)(UInt16); 1230 1231static BOOL connexionShiftIsDown = NO; 1232static UInt16 connexionClient = 0; 1233static UInt32 connexionSignature = 'SoSp'; 1234static UInt8 *connexionName = (UInt8 *)"SolveSpace"; 1235static CFBundleRef spaceBundle = NULL; 1236static InstallConnexionHandlersProc installConnexionHandlers = NULL; 1237static CleanupConnexionHandlersProc cleanupConnexionHandlers = NULL; 1238static RegisterConnexionClientProc registerConnexionClient = NULL; 1239static UnregisterConnexionClientProc unregisterConnexionClient = NULL; 1240 1241static void connexionAdded(io_connect_t con) {} 1242static void connexionRemoved(io_connect_t con) {} 1243static void connexionMessage(io_connect_t con, natural_t type, void *arg) { 1244 if (type != kConnexionMsgDeviceState) { 1245 return; 1246 } 1247 1248 ConnexionDeviceState *device = (ConnexionDeviceState *)arg; 1249 1250 dispatch_async(dispatch_get_main_queue(), ^(void){ 1251 SolveSpace::SS.GW.SpaceNavigatorMoved( 1252 (double)device->axis[0] * -0.25, 1253 (double)device->axis[1] * -0.25, 1254 (double)device->axis[2] * 0.25, 1255 (double)device->axis[3] * -0.0005, 1256 (double)device->axis[4] * -0.0005, 1257 (double)device->axis[5] * -0.0005, 1258 (connexionShiftIsDown == YES) ? 1 : 0 1259 ); 1260 }); 1261} 1262 1263static void connexionInit() { 1264 NSString *bundlePath = @"/Library/Frameworks/3DconnexionClient.framework"; 1265 NSURL *bundleURL = [NSURL fileURLWithPath:bundlePath]; 1266 spaceBundle = CFBundleCreate(kCFAllocatorDefault, (__bridge CFURLRef)bundleURL); 1267 1268 // Don't continue if no Spacemouse driver is installed on this machine 1269 if (spaceBundle == NULL) { 1270 return; 1271 } 1272 1273 installConnexionHandlers = (InstallConnexionHandlersProc) 1274 CFBundleGetFunctionPointerForName(spaceBundle, 1275 CFSTR("InstallConnexionHandlers")); 1276 1277 cleanupConnexionHandlers = (CleanupConnexionHandlersProc) 1278 CFBundleGetFunctionPointerForName(spaceBundle, 1279 CFSTR("CleanupConnexionHandlers")); 1280 1281 registerConnexionClient = (RegisterConnexionClientProc) 1282 CFBundleGetFunctionPointerForName(spaceBundle, 1283 CFSTR("RegisterConnexionClient")); 1284 1285 unregisterConnexionClient = (UnregisterConnexionClientProc) 1286 CFBundleGetFunctionPointerForName(spaceBundle, 1287 CFSTR("UnregisterConnexionClient")); 1288 1289 // Only continue if all required symbols have been loaded 1290 if ((installConnexionHandlers == NULL) || (cleanupConnexionHandlers == NULL) 1291 || (registerConnexionClient == NULL) || (unregisterConnexionClient == NULL)) { 1292 CFRelease(spaceBundle); 1293 spaceBundle = NULL; 1294 return; 1295 } 1296 1297 installConnexionHandlers(&connexionMessage, &connexionAdded, &connexionRemoved); 1298 connexionClient = registerConnexionClient(connexionSignature, connexionName, 1299 kConnexionClientModeTakeOver, kConnexionMaskButtons | kConnexionMaskAxis); 1300 1301 // Monitor modifier flags to detect Shift button state changes 1302 [NSEvent addLocalMonitorForEventsMatchingMask:(NSKeyDownMask | NSFlagsChangedMask) 1303 handler:^(NSEvent *event) { 1304 if (event.modifierFlags & NSShiftKeyMask) { 1305 connexionShiftIsDown = YES; 1306 } 1307 return event; 1308 }]; 1309 1310 [NSEvent addLocalMonitorForEventsMatchingMask:(NSKeyUpMask | NSFlagsChangedMask) 1311 handler:^(NSEvent *event) { 1312 if (!(event.modifierFlags & NSShiftKeyMask)) { 1313 connexionShiftIsDown = NO; 1314 } 1315 return event; 1316 }]; 1317} 1318 1319static void connexionClose() { 1320 if (spaceBundle == NULL) { 1321 return; 1322 } 1323 1324 unregisterConnexionClient(connexionClient); 1325 cleanupConnexionHandlers(); 1326 1327 CFRelease(spaceBundle); 1328} 1329 1330int main(int argc, const char *argv[]) { 1331 [NSApplication sharedApplication]; 1332 ApplicationDelegate *delegate = [[ApplicationDelegate alloc] init]; 1333 [NSApp setDelegate:delegate]; 1334 1335 SolveSpace::InitGraphicsWindow(); 1336 SolveSpace::InitTextWindow(); 1337 [[NSBundle mainBundle] loadNibNamed:@"MainMenu" owner:nil topLevelObjects:nil]; 1338 SolveSpace::InitMainMenu([NSApp mainMenu]); 1339 1340 connexionInit(); 1341 SolveSpace::SS.Init(); 1342 1343 [GW makeKeyAndOrderFront:nil]; 1344 [NSApp run]; 1345 1346 connexionClose(); 1347 SolveSpace::SK.Clear(); 1348 SolveSpace::SS.Clear(); 1349 1350 return 0; 1351} 1352