From 95ab80d0ee17dd43fe4158b545814f304baae15b Mon Sep 17 00:00:00 2001 From: whitequark Date: Tue, 24 Mar 2015 09:45:53 +0300 Subject: [PATCH] Implement OS X port. --- CMakeLists.txt | 5 +- cmake/MacOSXBundleInfo.plist.in | 47 + src/CMakeLists.txt | 73 +- src/cocoa/AppIcon.iconset/icon_16x16.png | 1 + src/cocoa/AppIcon.iconset/icon_32x32.png | 1 + src/cocoa/MainMenu.xib | 65 ++ src/cocoa/SaveFormatAccessory.xib | 50 + src/cocoa/cocoamain.mm | 1103 ++++++++++++++++++++++ src/fltk/fltkmain.cpp | 12 +- src/graphicswin.cpp | 12 +- src/gtk/gtkmain.cpp | 14 +- src/solvespace.cpp | 26 +- src/solvespace.h | 13 +- src/unix/gloffscreen.cpp | 20 +- src/unix/gloffscreen.h | 5 +- src/win32/w32main.cpp | 13 +- 16 files changed, 1420 insertions(+), 40 deletions(-) create mode 100644 cmake/MacOSXBundleInfo.plist.in create mode 120000 src/cocoa/AppIcon.iconset/icon_16x16.png create mode 120000 src/cocoa/AppIcon.iconset/icon_32x32.png create mode 100644 src/cocoa/MainMenu.xib create mode 100644 src/cocoa/SaveFormatAccessory.xib create mode 100644 src/cocoa/cocoamain.mm diff --git a/CMakeLists.txt b/CMakeLists.txt index 4535bb6e..8ae7cad8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -72,7 +72,10 @@ if(WIN32) "${CMAKE_SOURCE_DIR}/extlib/si") set(SPACEWARE_LIBRARIES "${CMAKE_SOURCE_DIR}/extlib/si/siapp.lib") -else() +elseif(APPLE) + find_package(PNG REQUIRED) + find_library(APPKIT_LIBRARY AppKit REQUIRED) +else() # Linux and compatible systems find_package(PNG REQUIRED) find_package(SpaceWare) diff --git a/cmake/MacOSXBundleInfo.plist.in b/cmake/MacOSXBundleInfo.plist.in new file mode 100644 index 00000000..c215c443 --- /dev/null +++ b/cmake/MacOSXBundleInfo.plist.in @@ -0,0 +1,47 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + solvespace + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + SolveSpace + CFBundlePackageType + APPL + CFBundleVersion + ${solvespace_VERSION_MAJOR}.${solvespace_VERSION_MINOR} + CFBundleShortVersionString + ${solvespace_VERSION_MAJOR}.${solvespace_VERSION_MINOR} + NSHumanReadableCopyright + © 2008-2015 Jonathan Westhues and other authors + NSPrincipalClass + NSApplication + NSMainNibFile + MainMenu + CFBundleIconFile + AppIcon + CFBundleDocumentTypes + + + CFBundleTypeExtensions + + slvs + + CFBundleTypeIconFile + AppIcon.icns + CFBundleTypeName + SolveSpace sketch + CFBundleTypeOSTypes + + slvs + + CFBundleTypeRole + Editor + + + + diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 9f9f5352..a118df83 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -129,6 +129,27 @@ if(WIN32) win32/freeze.cpp win32/w32main.cpp win32/resource.rc) +elseif(APPLE) + add_definitions( + -mmacosx-version-min=10.6 + -fobjc-arc) + + set(platform_SOURCES + cocoa/cocoamain.mm + unix/gloffscreen.cpp) + + set(platform_XIBS + cocoa/MainMenu.xib + cocoa/SaveFormatAccessory.xib) + + set(platform_ICONS + cocoa/AppIcon.iconset) + + set(platform_RESOURCES + unix/solvespace-48x48.png) + + set(platform_LIBRARIES + ${APPKIT_LIBRARY}) elseif(HAVE_FLTK) include_directories(${FLTK_INCLUDE_DIR}) @@ -168,6 +189,52 @@ elseif(HAVE_GTK) ${GLEW_LIBRARIES}) endif() +set(platform_BUNDLED_RESOURCES) + +foreach(xib ${platform_XIBS}) + get_filename_component(nib ${xib} NAME_WE) + set(source ${CMAKE_CURRENT_SOURCE_DIR}/${xib}) + set(target ${CMAKE_CURRENT_BINARY_DIR}/solvespace.app/Contents/Resources/${nib}.nib) + list(APPEND platform_BUNDLED_RESOURCES ${target}) + + add_custom_command( + OUTPUT ${target} + COMMAND mkdir -p ${CMAKE_CURRENT_BINARY_DIR}/solvespace.app/Contents/Resources + COMMAND ibtool --errors --warnings --notices + --output-format human-readable-text --compile + ${target} ${source} + COMMENT "Building Interface Builder file ${xib}" + DEPENDS ${xib}) +endforeach() + +foreach(icon ${platform_ICONS}) + get_filename_component(name ${icon} NAME_WE) + set(source ${CMAKE_CURRENT_SOURCE_DIR}/${icon}) + set(target ${CMAKE_CURRENT_BINARY_DIR}/solvespace.app/Contents/Resources/${name}.icns) + list(APPEND platform_BUNDLED_RESOURCES ${target}) + + add_custom_command( + OUTPUT ${target} + COMMAND mkdir -p ${CMAKE_CURRENT_BINARY_DIR}/solvespace.app/Contents/Resources + COMMAND iconutil -c icns -o ${target} ${source} + COMMENT "Building icon set ${icon}" + DEPENDS ${source}) +endforeach() + +foreach(res ${platform_RESOURCES}) + get_filename_component(name ${res} NAME) + set(source ${CMAKE_CURRENT_SOURCE_DIR}/${res}) + set(target ${CMAKE_CURRENT_BINARY_DIR}/solvespace.app/Contents/Resources/${name}) + list(APPEND platform_BUNDLED_RESOURCES ${target}) + + add_custom_command( + OUTPUT ${target} + COMMAND mkdir -p ${CMAKE_CURRENT_BINARY_DIR}/solvespace.app/Contents/Resources + COMMAND ${CMAKE_COMMAND} -E copy ${source} ${target} + COMMENT "Copying resource file ${res}" + DEPENDS ${res}) +endforeach() + # solvespace executable set(solvespace_HEADERS @@ -226,12 +293,13 @@ set(solvespace_SOURCES srf/surfinter.cpp srf/triangulate.cpp) -add_executable(solvespace WIN32 +add_executable(solvespace WIN32 MACOSX_BUNDLE ${libslvs_HEADERS} ${libslvs_SOURCES} ${util_SOURCES} ${platform_HEADERS} ${platform_SOURCES} + ${platform_BUNDLED_RESOURCES} ${generated_HEADERS} ${solvespace_HEADERS} ${solvespace_SOURCES}) @@ -252,7 +320,8 @@ if(SPACEWARE_FOUND) endif() install(TARGETS solvespace - RUNTIME DESTINATION bin) + RUNTIME DESTINATION bin + BUNDLE DESTINATION .) install(FILES unix/solvespace.desktop DESTINATION share/applications) diff --git a/src/cocoa/AppIcon.iconset/icon_16x16.png b/src/cocoa/AppIcon.iconset/icon_16x16.png new file mode 120000 index 00000000..d618a4c4 --- /dev/null +++ b/src/cocoa/AppIcon.iconset/icon_16x16.png @@ -0,0 +1 @@ +../../unix/solvespace-16x16.png \ No newline at end of file diff --git a/src/cocoa/AppIcon.iconset/icon_32x32.png b/src/cocoa/AppIcon.iconset/icon_32x32.png new file mode 120000 index 00000000..c205b7d4 --- /dev/null +++ b/src/cocoa/AppIcon.iconset/icon_32x32.png @@ -0,0 +1 @@ +../../unix/solvespace-32x32.png \ No newline at end of file diff --git a/src/cocoa/MainMenu.xib b/src/cocoa/MainMenu.xib new file mode 100644 index 00000000..b1951c40 --- /dev/null +++ b/src/cocoa/MainMenu.xib @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/cocoa/SaveFormatAccessory.xib b/src/cocoa/SaveFormatAccessory.xib new file mode 100644 index 00000000..3962b6a1 --- /dev/null +++ b/src/cocoa/SaveFormatAccessory.xib @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/cocoa/cocoamain.mm b/src/cocoa/cocoamain.mm new file mode 100644 index 00000000..8858c6ce --- /dev/null +++ b/src/cocoa/cocoamain.mm @@ -0,0 +1,1103 @@ +//----------------------------------------------------------------------------- +// Our main() function, and Cocoa-specific stuff to set up our windows and +// otherwise handle our interface to the operating system. Everything +// outside gtk/... should be standard C++ and OpenGL. +// +// Copyright 2015 +//----------------------------------------------------------------------------- +#include +#include + +#import + +#include +#include + +#include "solvespace.h" +#include "../unix/gloffscreen.h" +#include + +using SolveSpace::dbp; + +char SolveSpace::RecentFile[MAX_RECENT][MAX_PATH]; + +#define GL_CHECK() \ + do { \ + int err = (int)glGetError(); \ + if(err) dbp("%s:%d: glGetError() == 0x%X", __FILE__, __LINE__, err); \ + } while (0) + +/* Settings */ + +namespace SolveSpace { +void CnfFreezeInt(uint32_t val, const char *key) { + [[NSUserDefaults standardUserDefaults] + setInteger:val forKey:[NSString stringWithUTF8String:key]]; +} + +uint32_t CnfThawInt(uint32_t val, const char *key) { + NSString *nsKey = [NSString stringWithUTF8String:key]; + if([[NSUserDefaults standardUserDefaults] objectForKey:nsKey]) + return [[NSUserDefaults standardUserDefaults] integerForKey:nsKey]; + return val; +} + +void CnfFreezeFloat(float val, const char *key) { + [[NSUserDefaults standardUserDefaults] + setFloat:val forKey:[NSString stringWithUTF8String:key]]; +} + +float CnfThawFloat(float val, const char *key) { + NSString *nsKey = [NSString stringWithUTF8String:key]; + if([[NSUserDefaults standardUserDefaults] objectForKey:nsKey]) + return [[NSUserDefaults standardUserDefaults] floatForKey:nsKey]; + return val; +} + +void CnfFreezeString(const char *val, const char *key) { + [[NSUserDefaults standardUserDefaults] + setObject:[NSString stringWithUTF8String:val] + forKey:[NSString stringWithUTF8String:key]]; +} + +void CnfThawString(char *val, int valsz, const char *key) { + NSString *nsKey = [NSString stringWithUTF8String:key]; + if([[NSUserDefaults standardUserDefaults] objectForKey:nsKey]) { + NSString *nsVal = [[NSUserDefaults standardUserDefaults] stringForKey:nsKey]; + snprintf(val, valsz, "%s", [nsVal UTF8String]); + } +} +}; + +/* Timer */ + +int64_t SolveSpace::GetMilliseconds(void) { + clock_serv_t cclock; + mach_timespec_t mts; + + host_get_clock_service(mach_host_self(), SYSTEM_CLOCK, &cclock); + clock_get_time(cclock, &mts); + mach_port_deallocate(mach_task_self(), cclock); + + return mts.tv_sec * 1000 + mts.tv_nsec / 1000000; +} + +@interface DeferredHandler : NSObject ++ (void) runLater:(id)dummy; ++ (void) runCallback; +@end + +@implementation DeferredHandler ++ (void) runLater:(id)dummy { + SolveSpace::SS.DoLater(); +} ++ (void) runCallback { + SolveSpace::SS.GW.TimerCallback(); + SolveSpace::SS.TW.TimerCallback(); +} +@end + +void SolveSpace::SetTimerFor(int milliseconds) { + NSMethodSignature *signature = [[DeferredHandler class] + methodSignatureForSelector:@selector(runCallback)]; + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; + [invocation setSelector:@selector(runCallback)]; + [invocation setTarget:[DeferredHandler class]]; + [NSTimer scheduledTimerWithTimeInterval:(milliseconds / 1000.0) + invocation:invocation repeats:NO]; +} + +void SolveSpace::ScheduleLater() { + [[NSRunLoop currentRunLoop] + performSelector:@selector(runLater:) + target:[DeferredHandler class] argument:nil + order:0 modes:@[NSDefaultRunLoopMode]]; +} + +/* OpenGL view */ + +@interface GLViewWithEditor : NSView +- (void)drawGL; + +@property BOOL wantsBackingStoreScaling; + +@property(readonly, getter=isEditing) BOOL editing; +- (void)startEditing:(NSString*)text at:(NSPoint)origin; +- (void)stopEditing; +- (void)didEdit:(NSString*)text; +@end + +@implementation GLViewWithEditor +{ + GLOffscreen *offscreen; + NSOpenGLContext *glContext; +@protected + NSTextField *editor; +} + +- initWithFrame:(NSRect)frameRect { + self = [super initWithFrame:frameRect]; + [self setWantsLayer:YES]; + + NSOpenGLPixelFormatAttribute attrs[] = { + NSOpenGLPFAColorSize, 24, + NSOpenGLPFADepthSize, 24, + 0 + }; + NSOpenGLPixelFormat *pixelFormat = [[NSOpenGLPixelFormat alloc] initWithAttributes:attrs]; + glContext = [[NSOpenGLContext alloc] initWithFormat:pixelFormat shareContext:NULL]; + + editor = [[NSTextField alloc] init]; + [editor setEditable:YES]; + [editor setTarget:self]; + [editor setAction:@selector(editorAction:)]; + + return self; +} + +- (void)dealloc { + delete offscreen; +} + +#define CONVERT1(name, to_from) \ + - (NS##name)convert##name##to_from##Backing:(NS##name)input { \ + return _wantsBackingStoreScaling ? [super convert##name##to_from##Backing:input] : input; } +#define CONVERT(name) CONVERT1(name, To) CONVERT1(name, From) +CONVERT(Size) +CONVERT(Rect) +#undef CONVERT +#undef CONVERT1 + +- (NSPoint)convertPointToBacking:(NSPoint)input { + if(_wantsBackingStoreScaling) return [super convertPointToBacking:input]; + else { + input.y *= -1; + return input; + } +} + +- (NSPoint)convertPointFromBacking:(NSPoint)input { + if(_wantsBackingStoreScaling) return [super convertPointFromBacking:input]; + else { + input.y *= -1; + return input; + } +} + +- (void)drawRect:(NSRect)aRect { + [glContext makeCurrentContext]; + + if(!offscreen) + offscreen = new GLOffscreen; + + NSSize size = [self convertSizeToBacking:[self bounds].size]; + NSRect bounds = [self convertRectToBacking:[self bounds]]; + offscreen->begin(size.width, size.height); + + [self drawGL]; + GL_CHECK(); + + uint8_t *pixels = offscreen->end(![self isFlipped]); + CGDataProviderRef provider = CGDataProviderCreateWithData( + NULL, pixels, size.width * size.height * 4, NULL); + CGColorSpaceRef colorspace = CGColorSpaceCreateDeviceRGB(); + CGImageRef image = CGImageCreate(size.width, size.height, 8, 32, + size.width * 4, colorspace, kCGBitmapByteOrder32Little | kCGImageAlphaNoneSkipFirst, + provider, NULL, true, kCGRenderingIntentDefault); + + CGContextDrawImage((CGContextRef) [[NSGraphicsContext currentContext] graphicsPort], + [self bounds], image); +} + +- (void)drawGL { +} + +@synthesize editing; + +- (void)startEditing:(NSString*)text at:(NSPoint)origin { + if(!self->editing) { + [self addSubview:editor]; + self->editing = YES; + } + + [editor setFrameOrigin:origin]; + [editor setStringValue:text]; + [self prepareEditor]; + [[self window] becomeKeyWindow]; + [[self window] makeFirstResponder:editor]; +} + +- (void)stopEditing { + if(self->editing) { + [editor removeFromSuperview]; + self->editing = NO; + } +} + +- (void)editorAction:(id)sender { + [self didEdit:[editor stringValue]]; + [self stopEditing]; +} + +- (void)prepareEditor { + [editor setFrameSize:(NSSize){ + .width = 100, + .height = [editor intrinsicContentSize].height }]; +} + +- (void)didEdit:(NSString*)text { +} +@end + +/* Graphics window */ + +@interface GraphicsWindowView : GLViewWithEditor +{ + NSTrackingArea *trackingArea; +} + +@property(readonly) NSEvent *lastContextMenuEvent; +@end + +@implementation GraphicsWindowView +- (BOOL)isFlipped { + return YES; +} + +- (void)drawGL { + SolveSpace::SS.GW.Paint(); +} + +- (BOOL)acceptsFirstResponder { + return YES; +} + +- (void) createTrackingArea { + trackingArea = [[NSTrackingArea alloc] initWithRect:[self bounds] + options:(NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | + NSTrackingActiveInKeyWindow) + owner:self userInfo:nil]; + [self addTrackingArea:trackingArea]; +} + +- (void) updateTrackingAreas +{ + [self removeTrackingArea:trackingArea]; + [self createTrackingArea]; + [super updateTrackingAreas]; +} + +- (void)mouseMoved:(NSEvent*)event { + NSPoint point = [self ij_to_xy:[self convertPoint:[event locationInWindow] fromView:nil]]; + NSUInteger flags = [event modifierFlags]; + NSUInteger buttons = [NSEvent pressedMouseButtons]; + SolveSpace::SS.GW.MouseMoved(point.x, point.y, + buttons & (1 << 0), + buttons & (1 << 2), + buttons & (1 << 1), + flags & NSShiftKeyMask, + flags & NSCommandKeyMask); +} + +- (void)mouseDragged:(NSEvent*)event { + [self mouseMoved:event]; +} + +- (void)rightMouseDragged:(NSEvent*)event { + [self mouseMoved:event]; +} + +- (void)otherMouseDragged:(NSEvent*)event { + [self mouseMoved:event]; +} + +- (void)mouseDown:(NSEvent*)event { + NSPoint point = [self ij_to_xy:[self convertPoint:[event locationInWindow] fromView:nil]]; + if([event clickCount] == 1) + SolveSpace::SS.GW.MouseLeftDown(point.x, point.y); + else if([event clickCount] == 2) + SolveSpace::SS.GW.MouseLeftDoubleClick(point.x, point.y); +} + +- (void)rightMouseDown:(NSEvent*)event { + NSPoint point = [self ij_to_xy:[self convertPoint:[event locationInWindow] fromView:nil]]; + SolveSpace::SS.GW.MouseMiddleOrRightDown(point.x, point.y); +} + +- (void)otherMouseDown:(NSEvent*)event { + [self rightMouseDown:event]; +} + +- (void)mouseUp:(NSEvent*)event { + NSPoint point = [self ij_to_xy:[self convertPoint:[event locationInWindow] fromView:nil]]; + SolveSpace::SS.GW.MouseLeftUp(point.x, point.y); +} + +- (void)rightMouseUp:(NSEvent*)event { + NSPoint point = [self ij_to_xy:[self convertPoint:[event locationInWindow] fromView:nil]]; + self->_lastContextMenuEvent = event; + SolveSpace::SS.GW.MouseRightUp(point.x, point.y); +} + +- (void)scrollWheel:(NSEvent*)event { + NSPoint point = [self ij_to_xy:[self convertPoint:[event locationInWindow] fromView:nil]]; + SolveSpace::SS.GW.MouseScroll(point.x, point.y, -[event deltaY]); +} + +- (void)mouseExited:(NSEvent*)event { + SolveSpace::SS.GW.MouseLeave(); +} + +- (void)keyDown:(NSEvent*)event { + int chr = 0; + if(NSString *nsChr = [event charactersIgnoringModifiers]) + chr = [nsChr characterAtIndex:0]; + + if(chr == NSDeleteCharacter) /* map delete back to backspace */ + chr = '\b'; + if(chr >= NSF1FunctionKey && chr <= NSF12FunctionKey) + chr = SolveSpace::GraphicsWindow::FUNCTION_KEY_BASE + (chr - NSF1FunctionKey); + + NSUInteger flags = [event modifierFlags]; + if(flags & NSShiftKeyMask) + chr |= SolveSpace::GraphicsWindow::SHIFT_MASK; + if(flags & NSCommandKeyMask) + chr |= SolveSpace::GraphicsWindow::CTRL_MASK; + + // override builtin behavior: "focus on next cell", "close window" + if(chr == '\t' || chr == '\x1b') + [[NSApp mainMenu] performKeyEquivalent:event]; + else if(!chr || !SolveSpace::SS.GW.KeyDown(chr)) + [super keyDown:event]; +} + +- (void)startEditing:(NSString*)text at:(NSPoint)xy { + // Convert to ij (vs. xy) style coordinates + NSSize size = [self convertSizeToBacking:[self bounds].size]; + NSPoint point = { + .x = xy.x + size.width / 2, + .y = xy.y - size.height / 2 + [editor intrinsicContentSize].height + }; + [super startEditing:text at:[self convertPointFromBacking:point]]; +} + +- (void)didEdit:(NSString*)text { + SolveSpace::SS.GW.EditControlDone([text UTF8String]); +} + +- (void)cancelOperation:(id)sender { + [self stopEditing]; +} + +- (NSPoint)ij_to_xy:(NSPoint)ij { + // Convert to xy (vs. ij) style coordinates, + // with (0, 0) at center + NSSize size = [self bounds].size; + return [self convertPointToBacking:(NSPoint){ + .x = ij.x - size.width / 2, .y = ij.y - size.height / 2 }]; +} +@end + +@interface GraphicsWindowDelegate : NSObject +- (BOOL)windowShouldClose:(id)sender; + +@property(readonly, getter=isFullscreen) BOOL fullscreen; +- (void)windowDidEnterFullScreen:(NSNotification *)notification; +- (void)windowDidExitFullScreen:(NSNotification *)notification; +@end + +@implementation GraphicsWindowDelegate +- (BOOL)windowShouldClose:(id)sender { + [NSApp terminate:sender]; + return FALSE; /* in case NSApp changes its mind */ +} + +@synthesize fullscreen; +- (void)windowDidEnterFullScreen:(NSNotification *)notification { + fullscreen = true; + /* Update the menus */ + SolveSpace::SS.GW.EnsureValidActives(); +} +- (void)windowDidExitFullScreen:(NSNotification *)notification { + fullscreen = false; + /* Update the menus */ + SolveSpace::SS.GW.EnsureValidActives(); +} +@end + +static NSWindow *GW; +static GraphicsWindowView *GWView; +static GraphicsWindowDelegate *GWDelegate; + +namespace SolveSpace { +void InitGraphicsWindow() { + GW = [[NSWindow alloc] init]; + GWDelegate = [[GraphicsWindowDelegate alloc] init]; + [GW setDelegate:GWDelegate]; + [GW setStyleMask:(NSTitledWindowMask | NSClosableWindowMask | + NSMiniaturizableWindowMask | NSResizableWindowMask)]; + [GW setFrameAutosaveName:@"GraphicsWindow"]; + [GW setCollectionBehavior:NSWindowCollectionBehaviorFullScreenPrimary]; + if(![GW setFrameUsingName:[GW frameAutosaveName]]) + [GW setContentSize:(NSSize){ .width = 600, .height = 600 }]; + GWView = [[GraphicsWindowView alloc] init]; + [GW setContentView:GWView]; +} + +void GetGraphicsWindowSize(int *w, int *h) { + NSSize size = [GWView convertSizeToBacking:[GWView frame].size]; + *w = size.width; + *h = size.height; +} + +void InvalidateGraphics(void) { + [GWView setNeedsDisplay:YES]; +} + +void PaintGraphics(void) { + [GWView setNeedsDisplay:YES]; + CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, YES); +} + +void SetCurrentFilename(const char *filename) { + if(filename) { + [GW setTitleWithRepresentedFilename:[NSString stringWithUTF8String:filename]]; + } else { + [GW setTitle:@"(new sketch)"]; + [GW setRepresentedFilename:@""]; + } +} + +void ToggleFullScreen(void) { + [GW toggleFullScreen:nil]; +} + +bool FullScreenIsActive(void) { + return [GWDelegate isFullscreen]; +} + +void ShowGraphicsEditControl(int x, int y, char *str) { + [GWView startEditing:[NSString stringWithUTF8String:str] at:(NSPoint){x, y}]; +} + +void HideGraphicsEditControl(void) { + [GWView stopEditing]; +} + +bool GraphicsEditControlIsVisible(void) { + return [GWView isEditing]; +} +} + +/* Context menus */ + +static int contextMenuChoice; + +@interface ContextMenuResponder : NSObject ++ (void)handleClick:(id)sender; +@end + +@implementation ContextMenuResponder ++ (void)handleClick:(id)sender { + contextMenuChoice = [sender tag]; +} +@end + +namespace SolveSpace { +NSMenu *contextMenu, *contextSubmenu; + +void AddContextMenuItem(const char *label, int id_) { + NSMenuItem *menuItem; + if(label) { + menuItem = [[NSMenuItem alloc] initWithTitle:[NSString stringWithUTF8String:label] + action:@selector(handleClick:) keyEquivalent:@""]; + [menuItem setTarget:[ContextMenuResponder class]]; + [menuItem setTag:id_]; + } else { + menuItem = [NSMenuItem separatorItem]; + } + + if(id_ == CONTEXT_SUBMENU) { + [menuItem setSubmenu:contextSubmenu]; + contextSubmenu = nil; + } + + if(contextSubmenu) { + [contextSubmenu addItem:menuItem]; + } else { + if(!contextMenu) { + contextMenu = [[NSMenu alloc] + initWithTitle:[NSString stringWithUTF8String:label]]; + } + + [contextMenu addItem:menuItem]; + } +} + +void CreateContextSubmenu(void) { + if(contextSubmenu) oops(); + + contextSubmenu = [[NSMenu alloc] initWithTitle:@""]; +} + +int ShowContextMenu(void) { + if(!contextMenu) + return -1; + + [NSMenu popUpContextMenu:contextMenu + withEvent:[GWView lastContextMenuEvent] forView:GWView]; + + contextMenu = nil; + + return contextMenuChoice; +} +}; + +/* Main menu */ + +@interface MainMenuResponder : NSObject ++ (void)handleStatic:(id)sender; ++ (void)handleRecent:(id)sender; +@end + +@implementation MainMenuResponder ++ (void)handleStatic:(id)sender { + SolveSpace::GraphicsWindow::MenuEntry *entry = + (SolveSpace::GraphicsWindow::MenuEntry*)[sender tag]; + + if(entry->fn && ![(NSMenuItem*)sender hasSubmenu]) + entry->fn(entry->id); +} + ++ (void)handleRecent:(id)sender { + int id_ = [sender tag]; + if(id_ >= RECENT_OPEN && id_ < (RECENT_OPEN + MAX_RECENT)) + SolveSpace::SolveSpaceUI::MenuFile(id_); + else if(id_ >= RECENT_IMPORT && id_ < (RECENT_IMPORT + MAX_RECENT)) + SolveSpace::Group::MenuGroup(id_); +} +@end + +namespace SolveSpace { +std::map mainMenuItems; + +void InitMainMenu(NSMenu *mainMenu) { + NSMenuItem *menuItem = NULL; + NSMenu *levels[5] = {mainMenu, 0}; + NSString *label; + + const GraphicsWindow::MenuEntry *entry = &GraphicsWindow::menu[0]; + int current_level = 0; + while(entry->level >= 0) { + if(entry->level > current_level) { + NSMenu *menu = [[NSMenu alloc] initWithTitle:label]; + [menu setAutoenablesItems:NO]; + [menuItem setSubmenu:menu]; + + if(entry->level >= sizeof(levels) / sizeof(levels[0])) + oops(); + + levels[entry->level] = menu; + } + + current_level = entry->level; + + if(entry->label) { + /* OS X does not support mnemonics */ + label = [[NSString stringWithUTF8String:entry->label] + stringByReplacingOccurrencesOfString:@"&" withString:@""]; + + unichar accel_char = entry->accel & + ~(GraphicsWindow::SHIFT_MASK | GraphicsWindow::CTRL_MASK); + if(accel_char > GraphicsWindow::FUNCTION_KEY_BASE && + accel_char <= GraphicsWindow::FUNCTION_KEY_BASE + 12) + accel_char = NSF1FunctionKey + (accel_char - GraphicsWindow::FUNCTION_KEY_BASE - 1); + NSString *accel = [NSString stringWithCharacters:&accel_char length:1]; + + menuItem = [levels[entry->level] addItemWithTitle:label + action:NULL keyEquivalent:[accel lowercaseString]]; + + NSUInteger modifierMask = 0; + if(entry->accel & GraphicsWindow::SHIFT_MASK) + modifierMask |= NSShiftKeyMask; + else if(entry->accel & GraphicsWindow::CTRL_MASK) + modifierMask |= NSCommandKeyMask; + [menuItem setKeyEquivalentModifierMask:modifierMask]; + + [menuItem setTag:(NSInteger)entry]; + [menuItem setTarget:[MainMenuResponder class]]; + [menuItem setAction:@selector(handleStatic:)]; + } else { + [levels[entry->level] addItem:[NSMenuItem separatorItem]]; + } + + mainMenuItems[entry->id] = menuItem; + + ++entry; + } +} + +void EnableMenuById(int id_, bool enabled) { + [mainMenuItems[id_] setEnabled:enabled]; +} + +void CheckMenuById(int id_, bool checked) { + [mainMenuItems[id_] setState:(checked ? NSOnState : NSOffState)]; +} + +void RadioMenuById(int id_, bool selected) { + CheckMenuById(id_, selected); +} + +static void RefreshRecentMenu(int id_, int base) { + NSMenuItem *recent = mainMenuItems[id_]; + NSMenu *menu = [[NSMenu alloc] initWithTitle:@""]; + [recent setSubmenu:menu]; + + if(std::string(RecentFile[0]).empty()) { + NSMenuItem *placeholder = [[NSMenuItem alloc] + initWithTitle:@"(no recent files)" action:nil keyEquivalent:@""]; + [placeholder setEnabled:NO]; + [menu addItem:placeholder]; + } else { + for(int i = 0; i < MAX_RECENT; i++) { + if(std::string(RecentFile[i]).empty()) + break; + + NSMenuItem *item = [[NSMenuItem alloc] + initWithTitle:[[NSString stringWithUTF8String:RecentFile[i]] + stringByAbbreviatingWithTildeInPath] + action:nil keyEquivalent:@""]; + [item setTag:(base + i)]; + [item setAction:@selector(handleRecent:)]; + [item setTarget:[MainMenuResponder class]]; + [menu addItem:item]; + } + } +} + +void RefreshRecentMenus(void) { + RefreshRecentMenu(GraphicsWindow::MNU_OPEN_RECENT, RECENT_OPEN); + RefreshRecentMenu(GraphicsWindow::MNU_GROUP_RECENT, RECENT_IMPORT); +} + +void ToggleMenuBar(void) { + [NSMenu setMenuBarVisible:![NSMenu menuBarVisible]]; +} + +bool MenuBarIsVisible(void) { + return [NSMenu menuBarVisible]; +} +} + +/* Save/load */ + +bool SolveSpace::GetOpenFile(char *file, const char *defExtension, const char *selPattern) { + NSOpenPanel *panel = [NSOpenPanel openPanel]; + NSMutableArray *filters = [[NSMutableArray alloc] init]; + for(NSString *filter in [[NSString stringWithUTF8String:selPattern] + componentsSeparatedByString:@"\n"]) { + [filters addObjectsFromArray: + [[[filter componentsSeparatedByString:@"\t"] objectAtIndex:1] + componentsSeparatedByString:@","]]; + } + [filters removeObjectIdenticalTo:@"*"]; + [panel setAllowedFileTypes:filters]; + + if([panel runModal] == NSFileHandlingPanelOKButton) { + strcpy(file, [[NSFileManager defaultManager] + fileSystemRepresentationWithPath:[[panel URL] path]]); + return true; + } else { + return false; + } +} + +@interface SaveFormatController : NSViewController +@property NSSavePanel *panel; +@property NSArray *extensions; +@property (nonatomic) IBOutlet NSPopUpButton *button; +@property (nonatomic) NSInteger index; +@end + +@implementation SaveFormatController +@synthesize panel, extensions, button, index; +- (void)setIndex:(NSInteger)newIndex { + self->index = newIndex; + NSString *extension = [extensions objectAtIndex:newIndex]; + if(![extension isEqual:@"*"]) { + NSString *filename = [panel nameFieldStringValue]; + NSString *basename = [[filename componentsSeparatedByString:@"."] objectAtIndex:0]; + [panel setNameFieldStringValue:[basename stringByAppendingPathExtension:extension]]; + } +} +@end + +bool SolveSpace::GetSaveFile(char *file, const char *defExtension, const char *selPattern) { + NSSavePanel *panel = [NSSavePanel savePanel]; + [panel setNameFieldStringValue:[@"untitled" + stringByAppendingPathExtension:[NSString stringWithUTF8String:defExtension]]]; + + SaveFormatController *controller = + [[SaveFormatController alloc] initWithNibName:@"SaveFormatAccessory" bundle:nil]; + [controller setPanel:panel]; + [panel setAccessoryView:[controller view]]; + + NSMutableArray *extensions = [[NSMutableArray alloc] init]; + [controller setExtensions:extensions]; + + NSPopUpButton *button = [controller button]; + [button removeAllItems]; + for(NSString *filter in [[NSString stringWithUTF8String:selPattern] + componentsSeparatedByString:@"\n"]) { + NSArray *filterParts = [filter componentsSeparatedByString:@"\t"]; + NSString *filterName = [filterParts objectAtIndex:0]; + NSArray *filterExtensions = [[filterParts objectAtIndex:1] + componentsSeparatedByString:@","]; + [button addItemWithTitle: + [[NSString alloc] initWithFormat:@"%@ (%@)", filterName, + [filterExtensions componentsJoinedByString:@", "]]]; + [extensions addObject:[filterExtensions objectAtIndex:0]]; + } + [button selectItemAtIndex:[extensions + indexOfObject:[NSString stringWithUTF8String:defExtension]]]; + + if([panel runModal] == NSFileHandlingPanelOKButton) { + strcpy(file, [[NSFileManager defaultManager] + fileSystemRepresentationWithPath:[[panel URL] path]]); + return true; + } else { + return false; + } +} + +int SolveSpace::SaveFileYesNoCancel(void) { + NSAlert *alert = [[NSAlert alloc] init]; + if(!std::string(SolveSpace::SS.saveFile).empty()) { + [alert setMessageText: + [[@"Do you want to save the changes you made to the sketch “" + stringByAppendingString: + [[NSString stringWithUTF8String:SolveSpace::SS.saveFile] + stringByAbbreviatingWithTildeInPath]] + stringByAppendingString:@"”?"]]; + } else { + [alert setMessageText:@"Do you want to save the changes you made to the new sketch?"]; + } + [alert setInformativeText:@"Your changes will be lost if you don't save them."]; + [alert addButtonWithTitle:@"Save"]; + [alert addButtonWithTitle:@"Cancel"]; + [alert addButtonWithTitle:@"Don't Save"]; + switch([alert runModal]) { + case NSAlertFirstButtonReturn: + return SAVE_YES; + case NSAlertSecondButtonReturn: + return SAVE_CANCEL; + case NSAlertThirdButtonReturn: + return SAVE_NO; + } + abort(); /* unreachable */ +} + +/* Text window */ + +@interface TextWindowView : GLViewWithEditor +{ + NSTrackingArea *trackingArea; +} + +@property (nonatomic, getter=isCursorHand) BOOL cursorHand; +@end + +@implementation TextWindowView +- (BOOL)isFlipped { + return YES; +} + +- (void)drawGL { + SolveSpace::SS.TW.Paint(); +} + +- (BOOL)acceptsFirstMouse:(NSEvent*)event { + return YES; +} + +- (void) createTrackingArea { + trackingArea = [[NSTrackingArea alloc] initWithRect:[self bounds] + options:(NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | + NSTrackingActiveAlways) + owner:self userInfo:nil]; + [self addTrackingArea:trackingArea]; +} + +- (void) updateTrackingAreas +{ + [self removeTrackingArea:trackingArea]; + [self createTrackingArea]; + [super updateTrackingAreas]; +} + +- (void)mouseMoved:(NSEvent*)event { + NSPoint point = [self convertPointToBacking: + [self convertPoint:[event locationInWindow] fromView:nil]]; + SolveSpace::SS.TW.MouseEvent(/*leftClick*/ false, /*leftDown*/ false, + point.x, -point.y); +} + +- (void)mouseDown:(NSEvent*)event { + NSPoint point = [self convertPointToBacking: + [self convertPoint:[event locationInWindow] fromView:nil]]; + SolveSpace::SS.TW.MouseEvent(/*leftClick*/ true, /*leftDown*/ true, + point.x, -point.y); +} + +- (void)mouseDragged:(NSEvent*)event { + NSPoint point = [self convertPointToBacking: + [self convertPoint:[event locationInWindow] fromView:nil]]; + SolveSpace::SS.TW.MouseEvent(/*leftClick*/ false, /*leftDown*/ true, + point.x, -point.y); +} + +- (void)setCursorHand:(BOOL)cursorHand { + if(_cursorHand != cursorHand) { + if(cursorHand) + [[NSCursor pointingHandCursor] push]; + else + [NSCursor pop]; + } + _cursorHand = cursorHand; +} + +- (void)mouseExited:(NSEvent*)event { + [self setCursorHand:FALSE]; + SolveSpace::SS.TW.MouseLeave(); +} + +- (void)startEditing:(NSString*)text at:(NSPoint)point { + point = [self convertPointFromBacking:point]; + point.y = -point.y; + [super startEditing:text at:point]; + [[self window] makeKeyWindow]; + [[self window] makeFirstResponder:editor]; +} + +- (void)stopEditing { + [super stopEditing]; + [GW makeKeyWindow]; +} + +- (void)didEdit:(NSString*)text { + SolveSpace::SS.TW.EditControlDone([text UTF8String]); +} + +- (void)prepareEditor { + [editor setFrameSize:(NSSize){ + .width = [self bounds].size.width - [editor frame].origin.x, + .height = [editor intrinsicContentSize].height }]; +} + +- (void)cancelOperation:(id)sender { + [self stopEditing]; +} +@end + +@interface TextWindowDelegate : NSObject +- (BOOL)windowShouldClose:(id)sender; +- (void)windowDidResize:(NSNotification *)notification; +@end + +@implementation TextWindowDelegate +- (BOOL)windowShouldClose:(id)sender { + SolveSpace::GraphicsWindow::MenuView(SolveSpace::GraphicsWindow::MNU_SHOW_TEXT_WND); + return NO; +} + +- (void)windowDidResize:(NSNotification *)notification { + NSClipView *view = [[[notification object] contentView] contentView]; + NSView *document = [view documentView]; + NSSize size = [document frame].size; + size.width = [view frame].size.width; + [document setFrameSize:size]; +} +@end + +static NSPanel *TW; +static TextWindowView *TWView; +static TextWindowDelegate *TWDelegate; + +namespace SolveSpace { +void InitTextWindow() { + TW = [[NSPanel alloc] init]; + TWDelegate = [[TextWindowDelegate alloc] init]; + [TW setStyleMask:(NSTitledWindowMask | NSClosableWindowMask | NSResizableWindowMask | + NSUtilityWindowMask)]; + [[TW standardWindowButton:NSWindowMiniaturizeButton] setHidden:YES]; + [[TW standardWindowButton:NSWindowZoomButton] setHidden:YES]; + [TW setTitle:@"Browser"]; + [TW setFrameAutosaveName:@"TextWindow"]; + [TW setFloatingPanel:YES]; + [TW setBecomesKeyOnlyIfNeeded:YES]; + [GW addChildWindow:TW ordered:NSWindowAbove]; + + NSScrollView *scrollView = [[NSScrollView alloc] init]; + [TW setContentView:scrollView]; + [scrollView setBackgroundColor:[NSColor blackColor]]; + [scrollView setHasVerticalScroller:YES]; + [[scrollView contentView] setCopiesOnScroll:YES]; + + TWView = [[TextWindowView alloc] init]; + [scrollView setDocumentView:TWView]; + + [TW setDelegate:TWDelegate]; + if(![TW setFrameUsingName:[TW frameAutosaveName]]) + [TW setContentSize:(NSSize){ .width = 420, .height = 300 }]; + [TWView setFrame:[[scrollView contentView] frame]]; +} + +void ShowTextWindow(bool visible) { + if(visible) + [TW orderFront:nil]; + else + [TW close]; +} + +void GetTextWindowSize(int *w, int *h) { + NSSize size = [TWView convertSizeToBacking:[TWView frame].size]; + *w = size.width; + *h = size.height; +} + +void InvalidateText(void) { + NSSize size = [TWView convertSizeToBacking:[TWView frame].size]; + size.height = (SS.TW.top[SS.TW.rows - 1] + 1) * TextWindow::LINE_HEIGHT / 2; + [TWView setFrameSize:[TWView convertSizeFromBacking:size]]; + [TWView setNeedsDisplay:YES]; +} + +void MoveTextScrollbarTo(int pos, int maxPos, int page) { + /* unused; we draw the entire text window and scroll in Cocoa */ +} + +void SetMousePointerToHand(bool is_hand) { + [TWView setCursorHand:is_hand]; +} + +void ShowTextEditControl(int x, int y, char *str) { + return [TWView startEditing:[NSString stringWithUTF8String:str] at:(NSPoint){x, y}]; +} + +void HideTextEditControl(void) { + return [TWView stopEditing]; +} + +bool TextEditControlIsVisible(void) { + return [TWView isEditing]; +} +}; + +/* Miscellanea */ + +void SolveSpace::DoMessageBox(const char *str, int rows, int cols, bool error) { + NSAlert *alert = [[NSAlert alloc] init]; + [alert setAlertStyle:(error ? NSWarningAlertStyle : NSInformationalAlertStyle)]; + + /* do some additional formatting of the message these are + heuristics, but they are made failsafe and lead to nice results. */ + NSString *input = [NSString stringWithUTF8String:str]; + NSRange dot = [input rangeOfCharacterFromSet: + [NSCharacterSet characterSetWithCharactersInString:@".:"]]; + if(dot.location != NSNotFound) { + [alert setMessageText:[[input substringToIndex:dot.location + 1] + stringByReplacingOccurrencesOfString:@"\n" withString:@" "]]; + [alert setInformativeText: + [[input substringFromIndex:dot.location + 1] + stringByTrimmingCharactersInSet:[NSCharacterSet newlineCharacterSet]]]; + } else { + [alert setMessageText:[input + stringByReplacingOccurrencesOfString:@"\n" withString:@" "]]; + } + + [alert runModal]; +} + +void SolveSpace::OpenWebsite(const char *url) { + [[NSWorkspace sharedWorkspace] openURL: + [NSURL URLWithString:[NSString stringWithUTF8String:url]]]; +} + +void SolveSpace::LoadAllFontFiles(void) { + NSArray *fontNames = [[NSFontManager sharedFontManager] availableFonts]; + for(NSString *fontName in fontNames) { + CTFontDescriptorRef fontRef = + CTFontDescriptorCreateWithNameAndSize ((__bridge CFStringRef)fontName, 10.0); + CFURLRef url = (CFURLRef)CTFontDescriptorCopyAttribute(fontRef, kCTFontURLAttribute); + NSString *fontPath = [NSString stringWithString:[(NSURL *)CFBridgingRelease(url) path]]; + if([[fontPath pathExtension] isEqual:@"ttf"]) { + TtfFont tf; + ZERO(&tf); + strcpy(tf.fontFile, [[NSFileManager defaultManager] + fileSystemRepresentationWithPath:fontPath]); + SS.fonts.l.Add(&tf); + } + } +} + +/* Application lifecycle */ + +@interface ApplicationDelegate : NSObject +- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)theApplication; +- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender; +- (void)applicationWillTerminate:(NSNotification *)aNotification; +- (BOOL)application:(NSApplication *)theApplication openFile:(NSString *)filename; +- (IBAction)preferences:(id)sender; +@end + +@implementation ApplicationDelegate +- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)theApplication { + return YES; +} + +- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender { + if(SolveSpace::SS.OkayToStartNewFile()) + return NSTerminateNow; + else + return NSTerminateCancel; +} + +- (void)applicationWillTerminate:(NSNotification *)aNotification { + SolveSpace::SK.Clear(); + SolveSpace::SS.Clear(); + SolveSpace::SS.Exit(); +} + +- (BOOL)application:(NSApplication *)theApplication openFile:(NSString *)filename { + return SolveSpace::SS.OpenFile([filename UTF8String]); +} + +- (IBAction)preferences:(id)sender { + SolveSpace::SS.TW.GoToScreen(SolveSpace::TextWindow::SCREEN_CONFIGURATION); + SolveSpace::SS.ScheduleShowTW(); +} +@end + +void SolveSpace::ExitNow(void) { + [NSApp stop:nil]; +} + +int main(int argc, const char *argv[]) { + [NSApplication sharedApplication]; + ApplicationDelegate *delegate = [[ApplicationDelegate alloc] init]; + [NSApp setDelegate:delegate]; + + SolveSpace::InitGraphicsWindow(); + SolveSpace::InitTextWindow(); + [[NSBundle mainBundle] loadNibNamed:@"MainMenu" owner:nil topLevelObjects:nil]; + SolveSpace::InitMainMenu([NSApp mainMenu]); + + SolveSpace::SS.Init(); + + [GW makeKeyAndOrderFront:nil]; + [NSApp activateIgnoringOtherApps:YES]; + [NSApp run]; + + return 0; +} diff --git a/src/fltk/fltkmain.cpp b/src/fltk/fltkmain.cpp index 55168d01..313214ce 100644 --- a/src/fltk/fltkmain.cpp +++ b/src/fltk/fltkmain.cpp @@ -294,8 +294,12 @@ static void LoadPreferences(void) Preferences = new Fl_Preferences(dir, "solvespace.org", "solvespace"); } -void SetWindowTitle(const char *str) { - GraphicsWnd->label(str); +void SetCurrentFilename(const char *filename) { + if(filename) { + GraphicsWnd->label(std::string("SolveSpace - ") + filename); + } else { + GraphicsWnd->label("SolveSpace - (not yet saved)"); + } } void SetMousePointerToHand(bool yes) { @@ -1331,7 +1335,9 @@ int main(int argc, char **argv) #endif // Call in to the platform-independent code, and let them do their init - SS.Init(file); + SS.Init(); + if(strcmp(file, "")) + SS.OpenFile(file); // And now it's the main event loop. All calls in to the rest of the // code will be from the callbacks. diff --git a/src/graphicswin.cpp b/src/graphicswin.cpp index 2b9f065f..3b07fa87 100644 --- a/src/graphicswin.cpp +++ b/src/graphicswin.cpp @@ -39,8 +39,10 @@ const GraphicsWindow::MenuEntry GraphicsWindow::menu[] = { { 1, "Export 3d &Wireframe...", MNU_EXPORT_WIREFRAME, 0, IN, mFile }, { 1, "Export Triangle &Mesh...", MNU_EXPORT_MESH, 0, IN, mFile }, { 1, "Export &Surfaces...", MNU_EXPORT_SURFACES,0, IN, mFile }, +#ifndef __APPLE__ { 1, NULL, 0, 0, IN, NULL }, { 1, "E&xit", MNU_EXIT, C|'Q', IN, mFile }, +#endif { 0, "&Edit", 0, 0, IN, NULL }, { 1, "&Undo", MNU_UNDO, C|'Z', IN, mEdit }, @@ -77,7 +79,7 @@ const GraphicsWindow::MenuEntry GraphicsWindow::menu[] = { { 1, "Show Snap &Grid", MNU_SHOW_GRID, '>', IC, mView }, { 1, "Use &Perspective Projection", MNU_PERSPECTIVE_PROJ,'`', IC, mView }, { 1, NULL, 0, 0, IN, NULL }, -#if defined(HAVE_FLTK) +#if defined(HAVE_FLTK) || defined(__APPLE__) { 1, "Show Menu &Bar", MNU_SHOW_MENU_BAR, F(12), IC, mView }, #endif { 1, "Show &Toolbar", MNU_SHOW_TOOLBAR, 0, IC, mView }, @@ -85,7 +87,7 @@ const GraphicsWindow::MenuEntry GraphicsWindow::menu[] = { { 1, NULL, 0, 0, IN, NULL }, { 1, "Dimensions in &Inches", MNU_UNITS_INCHES, 0, IR, mView }, { 1, "Dimensions in &Millimeters", MNU_UNITS_MM, 0, IR, mView }, -#if defined(HAVE_FLTK_FULLSCREEN) || defined(HAVE_GTK) +#if defined(HAVE_FLTK_FULLSCREEN) || defined(HAVE_GTK) || defined(__APPLE__) { 1, NULL, 0, 0, IN, NULL }, { 1, "&Full Screen", MNU_FULL_SCREEN, F(11), IC, mView }, #endif @@ -157,7 +159,9 @@ const GraphicsWindow::MenuEntry GraphicsWindow::menu[] = { { 0, "&Help", 0, 0, IN, NULL }, { 1, "&Website / Manual", MNU_WEBSITE, 0, IN, mHelp }, +#ifndef __APPLE__ { 1, "&About", MNU_ABOUT, 0, IN, mHelp }, +#endif { -1, 0, 0, 0, IN, 0 } }; @@ -624,13 +628,13 @@ void GraphicsWindow::EnsureValidActives(void) { ShowTextWindow(SS.GW.showTextWindow); CheckMenuById(MNU_SHOW_TEXT_WND, SS.GW.showTextWindow); -#if defined(HAVE_FLTK) +#if defined(HAVE_FLTK) || defined(__APPLE__) CheckMenuById(MNU_SHOW_MENU_BAR, MenuBarIsVisible()); #endif CheckMenuById(MNU_SHOW_TOOLBAR, SS.showToolbar); CheckMenuById(MNU_PERSPECTIVE_PROJ, SS.usePerspectiveProj); CheckMenuById(MNU_SHOW_GRID, SS.GW.showSnapGrid); -#if defined(HAVE_FLTK_FULLSCREEN) || defined(HAVE_GTK) +#if defined(HAVE_FLTK_FULLSCREEN) || defined(HAVE_GTK) || defined(__APPLE__) CheckMenuById(MNU_FULL_SCREEN, FullScreenIsActive()); #endif diff --git a/src/gtk/gtkmain.cpp b/src/gtk/gtkmain.cpp index 5f546ba6..8cc50e8c 100644 --- a/src/gtk/gtkmain.cpp +++ b/src/gtk/gtkmain.cpp @@ -716,8 +716,12 @@ void PaintGraphics(void) { Glib::MainContext::get_default()->iteration(false); } -void SetWindowTitle(const char *str) { - GW->set_title(str); +void SetCurrentFilename(const char *filename) { + if(filename) { + GW->set_title(std::string("SolveSpace - ") + filename); + } else { + GW->set_title("SolveSpace - (not yet saved)"); + } } void ToggleFullScreen(void) { @@ -1490,15 +1494,15 @@ int main(int argc, char** argv) { TW->show_all(); GW->show_all(); + SS.Init(); + if(argc >= 2) { if(argc > 2) { std::cerr << "Only the first file passed on command line will be opened." << std::endl; } - SS.Init(argv[1]); - } else { - SS.Init(""); + SS.OpenFile(argv[1]); } main.run(*GW); diff --git a/src/solvespace.cpp b/src/solvespace.cpp index 7059b724..ce37be4c 100644 --- a/src/solvespace.cpp +++ b/src/solvespace.cpp @@ -9,7 +9,7 @@ SolveSpaceUI SolveSpace::SS; Sketch SolveSpace::SK; -void SolveSpaceUI::Init(const char *cmdLine) { +void SolveSpaceUI::Init() { SS.tangentArcRadius = 10.0; // Then, load the registry settings. @@ -95,18 +95,20 @@ void SolveSpaceUI::Init(const char *cmdLine) { // configuration file, but we will automatically load those as we need // them. - // Start with either an empty file, or the file specified on the - // command line. NewFile(); AfterNewFile(); - if(strlen(cmdLine) != 0) { - if(LoadFromFile(cmdLine)) { - strcpy(saveFile, cmdLine); - } else { - NewFile(); - } +} + +bool SolveSpaceUI::OpenFile(const char *filename) { + bool success = LoadFromFile(filename); + if(success) { + AddToRecentList(filename); + strcpy(saveFile, filename); + } else { + NewFile(); } AfterNewFile(); + return success; } void SolveSpaceUI::Exit(void) { @@ -370,11 +372,9 @@ bool SolveSpaceUI::OkayToStartNewFile(void) { void SolveSpaceUI::UpdateWindowTitle(void) { if(strlen(saveFile) == 0) { - SetWindowTitle("SolveSpace - (not yet saved)"); + SetCurrentFilename(NULL); } else { - char buf[MAX_PATH+100]; - sprintf(buf, "SolveSpace - %s", saveFile); - SetWindowTitle(buf); + SetCurrentFilename(saveFile); } } diff --git a/src/solvespace.h b/src/solvespace.h index 8c11552d..06fe788e 100644 --- a/src/solvespace.h +++ b/src/solvespace.h @@ -150,6 +150,14 @@ int SaveFileYesNoCancel(void); # define PAT1(desc,e1) desc "\t*." e1 "\n" # define PAT2(desc,e1,e2) desc "\t*." e1 "\t*." e2 "\n" # define ENDPAT "All Files\t*" +#elif defined(__APPLE__) + // Selection pattern format to be parsed by Cocoa glue code: + // "PNG File\tpng\n" + // "JPEG file\tjpg,jpeg\n" + // "All Files\t*" +# define PAT1(desc,e1) desc "\t" e1 "\n" +# define PAT2(desc,e1,e2) desc "\t" e1 "," e2 "\n" +# define ENDPAT "All Files\t*" #else // Selection pattern format for Win32's OPENFILENAME.lpstrFilter: // "PNG File (*.png)\0*.png\0" @@ -238,7 +246,7 @@ void dbp(const char *str, ...); dbp("tri: (%.3f %.3f %.3f) (%.3f %.3f %.3f) (%.3f %.3f %.3f)", \ CO((tri).a), CO((tri).b), CO((tri).c)) -void SetWindowTitle(const char *str); +void SetCurrentFilename(const char *filename); void SetMousePointerToHand(bool yes); void DoMessageBox(const char *str, int rows, int cols, bool error); void SetTimerFor(int milliseconds); @@ -787,7 +795,8 @@ public: bool tangentArcDeleteOld; // The platform-dependent code calls this before entering the msg loop - void Init(const char *cmdLine); + void Init(void); + bool OpenFile(const char *filename); void Exit(void); // File load/save routines, including the additional files that get diff --git a/src/unix/gloffscreen.cpp b/src/unix/gloffscreen.cpp index 34263150..9757b035 100644 --- a/src/unix/gloffscreen.cpp +++ b/src/unix/gloffscreen.cpp @@ -3,14 +3,20 @@ // // Copyright 2015 //----------------------------------------------------------------------------- +#ifdef __APPLE__ +#include +#else #include +#endif #include "gloffscreen.h" #include "solvespace.h" GLOffscreen::GLOffscreen() : _pixels(NULL), _pixels_inv(NULL) { +#ifndef __APPLE__ if(glewInit() != GLEW_OK) oops(); +#endif if(!GL_EXT_framebuffer_object) oops(); @@ -57,7 +63,9 @@ bool GLOffscreen::begin(int width, int height) { return false; } -uint8_t *GLOffscreen::end() { +uint8_t *GLOffscreen::end(bool flip) { + uint32_t *pixels_tgt = flip ? _pixels_inv : _pixels; + #if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ glReadPixels(0, 0, _width, _height, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, _pixels_inv); @@ -66,11 +74,13 @@ uint8_t *GLOffscreen::end() { GL_BGRA, GL_UNSIGNED_INT_8_8_8_8, _pixels_inv); #endif - /* in OpenGL coordinates, bottom is zero Y */ - for(int i = 0; i < _height; i++) - memcpy(&_pixels[_width * i], &_pixels_inv[_width * (_height - i - 1)], _width * 4); + if(flip) { + /* in OpenGL coordinates, bottom is zero Y */ + for(int i = 0; i < _height; i++) + memcpy(&_pixels[_width * i], &_pixels_inv[_width * (_height - i - 1)], _width * 4); + } glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0); - return (uint8_t*) _pixels; + return (uint8_t*) (flip ? _pixels : _pixels_inv); } diff --git a/src/unix/gloffscreen.h b/src/unix/gloffscreen.h index 5f325b99..c897be3b 100644 --- a/src/unix/gloffscreen.h +++ b/src/unix/gloffscreen.h @@ -21,9 +21,10 @@ public: bool begin(int width, int height); /* get pixels out of the frame and restore OpenGL state. - the pixel format is ARGB32 with top row at index 0. + the pixel format is ARGB32 with top row at index 0 if + flip is true and bottom row at index 0 if flip is false. the returned array is valid until the next call to begin() */ - uint8_t *end(); + uint8_t *end(bool flip = true); private: unsigned int _framebuffer; diff --git a/src/win32/w32main.cpp b/src/win32/w32main.cpp index e3e5bf26..0954fd76 100644 --- a/src/win32/w32main.cpp +++ b/src/win32/w32main.cpp @@ -307,8 +307,13 @@ float SolveSpace::CnfThawFloat(float v, const char *name) { return u.f; } -void SetWindowTitle(const char *str) { - SetWindowText(GraphicsWnd, str); +void SolveSpace::SetCurrentFilename(const char *filename) { + if(filename) { + std::string title = std::string("SolveSpace - ") + filename; + SetWindowText(GraphicsWnd, title.c_str()); + } else { + SetWindowText(GraphicsWnd, "SolveSpace - (not yet saved)"); + } } void SolveSpace::SetMousePointerToHand(bool yes) { @@ -1227,7 +1232,9 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, #endif // Call in to the platform-independent code, and let them do their init - SS.Init(file); + SS.Init(); + if(strcmp(file, "")) + SS.OpenFile(file); // And now it's the message loop. All calls in to the rest of the code // will be from the wndprocs.