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