#include "pch.h" #include "log.h" #include "shader.h" #include "shape.h" #include "texture.h" #include "image.h" #include "app.h" #include "keymap.h" #include "main.h" #include "settings.h" #include #include #include #include #import "objc_utils.h" #include #include @import AppCenter; @import AppCenterAnalytics; @import AppCenterCrashes; @implementation View { NSSharingService *airdrop_service; } - (std::string)clipboard_get_string { NSString* ns = [[NSPasteboard generalPasteboard] stringForType:NSPasteboardTypeString]; const char* ptr = [ns cStringUsingEncoding:NSUTF8StringEncoding]; return std::string(ptr); } - (bool)clipboard_set_string:(const std::string &)s { NSString* ns = [NSString stringWithUTF8String:s.c_str()]; [[NSPasteboard generalPasteboard] setString:ns forType:NSPasteboardTypeString]; return true; } - (void)hockeyapp_crash { [MSCrashes generateTestCrash]; } - (void)async_lock { CGLLockContext([glctx CGLContextObj]); [glctx makeCurrentContext]; } - (void)async_unlock { CGLUnlockContext([glctx CGLContextObj]); } - (void)async_swap { CGLFlushDrawable([glctx CGLContextObj]); } - (void)close { [[NSApplication sharedApplication] terminate:nil]; } - (void)show_cursor:(bool)visible { if (cursor_visible == visible) return; cursor_visible = visible; if (cursor_inside && !cursor_ignore) visible ? [NSCursor unhide] : [NSCursor hide]; } - (std::string)pick_file:(NSArray*)types { NSOpenPanel *panel = [NSOpenPanel openPanel]; [panel setCanChooseFiles:YES]; [panel setCanChooseDirectories:NO]; [panel setAllowsMultipleSelection:NO]; // yes if more than one dir is allowed //NSArray* fileTypes = [NSArray arrayWithObjects:@"png", @"PNG", @"jpg", @"JPG", @"jpeg", nil]; [panel setAllowedFileTypes:types]; [self removeTrackingArea:trackingArea]; if (!cursor_visible) [NSCursor unhide]; NSInteger clicked = [panel runModal]; [self addTrackingArea:trackingArea]; if (!cursor_visible) [NSCursor hide]; std::string ret; if (clicked == NSFileHandlingPanelOKButton) { for (NSURL *url in [panel URLs]) { LOG("selected file: %s", [[url path] cStringUsingEncoding:NSUTF8StringEncoding]); ret = [[url path] cStringUsingEncoding:NSUTF8StringEncoding]; } } return ret; } - (std::string)pick_file_save:(NSArray*)types { NSSavePanel *panel = [NSSavePanel savePanel]; [panel setCanCreateDirectories:YES]; [panel setAllowedFileTypes:types]; [self removeTrackingArea:trackingArea]; if (!cursor_visible) [NSCursor unhide]; NSInteger clicked = [panel runModal]; [self addTrackingArea:trackingArea]; if (!cursor_visible) [NSCursor hide]; std::string ret; if (clicked == NSFileHandlingPanelOKButton) { NSURL *url = [panel URL]; LOG("selected file: %s", [[url path] cStringUsingEncoding:NSUTF8StringEncoding]); ret = [[url path] cStringUsingEncoding:NSUTF8StringEncoding]; } return ret; } - (std::string)pick_dir { NSOpenPanel *panel = [NSOpenPanel openPanel]; [panel setCanChooseFiles:NO]; [panel setCanChooseDirectories:YES]; [panel setAllowsMultipleSelection:NO]; // yes if more than one dir is allowed [self removeTrackingArea:trackingArea]; if (!cursor_visible) [NSCursor unhide]; NSInteger clicked = [panel runModal]; [self addTrackingArea:trackingArea]; if (!cursor_visible) [NSCursor hide]; std::string ret; if (clicked == NSFileHandlingPanelOKButton) { for (NSURL *url in [panel URLs]) { LOG("selected path: %s", [[url path] cStringUsingEncoding:NSUTF8StringEncoding]); ret = [[url path] cStringUsingEncoding:NSUTF8StringEncoding]; } } return ret; } - (void)share_file:(NSString*)file_path { NSURL *url = [NSURL fileURLWithPath:file_path]; NSArray *objectsToShare = @[url]; [self removeTrackingArea:trackingArea]; cursor_ignore = true; if (!cursor_visible) [NSCursor unhide]; [airdrop_service performWithItems:objectsToShare]; } - (NSWindow *)sharingService:(NSSharingService *)sharingService sourceWindowForShareItems:(NSArray *)items sharingContentScope:(NSSharingContentScope *)sharingContentScope { return self.window; } - (NSView *)anchoringViewForSharingService:(NSSharingService *)sharingService showRelativeToRect:(NSRect *)positioningRect preferredEdge:(NSRectEdge *)preferredEdge { return self; } -(void)sharingService:(NSSharingService *)sharingService willShareItems:(NSArray *)items { } - (void)sharingService:(NSSharingService *)sharingService didFailToShareItems:(NSArray *)items error:(NSError *)error { [self addTrackingArea:trackingArea]; cursor_ignore = false; if (!cursor_visible) [NSCursor hide]; } - (void)sharingService:(NSSharingService *)sharingService didShareItems:(NSArray *)items { [self addTrackingArea:trackingArea]; cursor_ignore = false; if (!cursor_visible) [NSCursor hide]; } - (instancetype)initWithFrame:(NSRect)frameRect { airdrop_service = [NSSharingService sharingServiceNamed:NSSharingServiceNameSendViaAirDrop]; airdrop_service.delegate = self; cursor_visible = CGCursorIsVisible(); cursor_inside = false; cursor_ignore = false; NSOpenGLPixelFormatAttribute attrs[] = { NSOpenGLPFADoubleBuffer, NSOpenGLPFADepthSize, 24, // Must specify the 3.2 Core Profile to use OpenGL 3.2 NSOpenGLPFAOpenGLProfile, NSOpenGLProfileVersion3_2Core, // Multisample NSOpenGLPFAMultisample, NSOpenGLPFASamples, 2, NSOpenGLPFASampleBuffers, 1, 0 }; NSOpenGLPixelFormat *pf = [[NSOpenGLPixelFormat alloc] initWithAttributes:attrs]; NSOpenGLContext* context = [[NSOpenGLContext alloc] initWithFormat:pf shareContext:nil]; self = [super initWithFrame:frameRect pixelFormat:pf]; [self setPixelFormat:pf]; [self setOpenGLContext:context]; [self updateTrackingAreas]; return self; } - (void)prepareOpenGL { [super prepareOpenGL]; NSLog(@"prepare"); // Synchronize buffer swaps with vertical refresh rate GLint swapInt = 1; [[self openGLContext] setValues:&swapInt forParameter:NSOpenGLCPSwapInterval]; glctx = [self openGLContext]; App::I->render_thread_start(); App::I->render_sync(); } - (void)terminateGL { App::I->ui_thread_stop(); App::I->render_thread_stop(); App::I->terminate(); delete App::I; App::I = nullptr; } - (void)drawRect:(NSRect)dirtyRect { return; } - (void)updateTrackingAreas { if(trackingArea != nil) [self removeTrackingArea:trackingArea]; trackingArea = [[NSTrackingArea alloc] initWithRect:[self bounds] options: (NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingActiveInKeyWindow) owner:self userInfo:nil]; [self addTrackingArea:trackingArea]; } - (void)reshape { [super reshape]; // Get the view size in Points NSRect viewRectPoints = [self bounds]; // We draw on a secondary thread through the display link. However, when // resizing the view, -drawRect is called on the main thread. // Add a mutex around to avoid the threads accessing the context // simultaneously when resizing. #if SUPPORT_RETINA_RESOLUTION // Rendering at retina resolutions will reduce aliasing, but at the potential // cost of framerate and battery life due to the GPU needing to render more // pixels. // Any calculations the renderer does which use pixel dimentions, must be // in "retina" space. [NSView convertRectToBacking] converts point sizes // to pixel sizes. Thus the renderer gets the size in pixels, not points, // so that it can set it's viewport and perform and other pixel based // calculations appropriately. // viewRectPixels will be larger than viewRectPoints for retina displays. // viewRectPixels will be the same as viewRectPoints for non-retina displays NSRect viewRectPixels = [self convertRectToBacking:viewRectPoints]; #else //if !SUPPORT_RETINA_RESOLUTION // App will typically render faster and use less power rendering at // non-retina resolutions since the GPU needs to render less pixels. // There is the cost of more aliasing, but it will be no-worse than // on a Mac without a retina display. // Points:Pixels is always 1:1 when not supporting retina resolutions NSRect viewRectPixels = viewRectPoints; #endif // !SUPPORT_RETINA_RESOLUTION App::I->ui_task_async([=] { App::I->resize(viewRectPixels.size.width, viewRectPixels.size.height); }); } - (void)renewGState { // Called whenever graphics state updated (such as window resize) // OpenGL rendering is not synchronous with other rendering on the OSX. // Therefore, call disableScreenUpdatesUntilFlush so the window server // doesn't render non-OpenGL content in the window asynchronously from // OpenGL content, which could cause flickering. (non-OpenGL content // includes the title bar and drawing done by the app with other APIs) [[self window] disableScreenUpdatesUntilFlush]; [super renewGState]; } - (void)mouseDown:(NSEvent *)theEvent { auto mouseLoc = [self convertPoint:[theEvent locationInWindow] fromView:nil]; App::I->ui_task_async([mouseLoc,p=theEvent.pressure] { App::I->mouse_down(0, mouseLoc.x, App::I->height - mouseLoc.y - 1, p, kEventSource::Mouse, 0); }); } - (void)rightMouseDown:(NSEvent *)theEvent { auto mouseLoc = [self convertPoint:[theEvent locationInWindow] fromView:nil]; App::I->ui_task_async([mouseLoc,p=theEvent.pressure] { App::I->mouse_down(1, mouseLoc.x, App::I->height - mouseLoc.y - 1, p, kEventSource::Mouse, 0); }); } - (void)mouseUp:(NSEvent *)theEvent { auto mouseLoc = [self convertPoint:[theEvent locationInWindow] fromView:nil]; App::I->ui_task_async([mouseLoc] { App::I->mouse_up(0, mouseLoc.x, App::I->height - mouseLoc.y - 1, kEventSource::Mouse, 0); }); } - (void)rightMouseUp:(NSEvent *)theEvent { auto mouseLoc = [self convertPoint:[theEvent locationInWindow] fromView:nil]; App::I->ui_task_async([mouseLoc] { App::I->mouse_up(1, mouseLoc.x, App::I->height - mouseLoc.y - 1, kEventSource::Mouse, 0); }); } - (void)mouseMoved:(NSEvent *)theEvent { auto mouseLoc = [self convertPoint:[theEvent locationInWindow] fromView:nil]; App::I->ui_task_async([mouseLoc,p=theEvent.pressure] { App::I->mouse_move(mouseLoc.x, App::I->height - mouseLoc.y - 1, p, kEventSource::Mouse, 0); }); } -(void)mouseDragged:(NSEvent *)theEvent { auto mouseLoc = [self convertPoint:[theEvent locationInWindow] fromView:nil]; App::I->ui_task_async([mouseLoc,p=theEvent.pressure] { App::I->mouse_move(mouseLoc.x, App::I->height - mouseLoc.y - 1, p, kEventSource::Mouse, 0); }); } - (void)rightMouseDragged:(NSEvent *)theEvent { auto mouseLoc = [self convertPoint:[theEvent locationInWindow] fromView:nil]; App::I->ui_task_async([mouseLoc,p=theEvent.pressure] { App::I->mouse_move(mouseLoc.x, App::I->height - mouseLoc.y - 1, p, kEventSource::Mouse, 0); }); } -(void)mouseExited:(NSEvent *)event { if (!cursor_visible) [NSCursor unhide]; cursor_inside = false; } -(void)mouseEntered:(NSEvent *)event { if (!cursor_visible) [NSCursor hide]; cursor_inside = true; } - (void)scrollWheel:(NSEvent *)theEvent { auto mouseLoc = [self convertPoint:[theEvent locationInWindow] fromView:nil]; App::I->ui_task_async([mouseLoc,d=[theEvent deltaY]] { App::I->mouse_scroll(mouseLoc.x, App::I->height - mouseLoc.y - 1, d); }); } - (void)keyDown:(NSEvent *)theEvent { auto keyCode = [theEvent keyCode]; auto chars = [theEvent characters]; const char* c_str = [chars cStringUsingEncoding:NSASCIIStringEncoding]; std::string s = c_str ? c_str : ""; App::I->ui_task_async([keyCode, s] { if (App::I->keys[(int)kKey::KeyCtrl]) { App::I->key_down(convert_key(keyCode)); App::I->key_up(convert_key(keyCode)); } else { App::I->key_down(convert_key(keyCode)); if (!s.empty()) App::I->key_char(s[0]); } }); } - (void)keyUp:(NSEvent *)theEvent { auto keyCode = [theEvent keyCode]; App::I->ui_task_async([keyCode] { App::I->key_up(convert_key(keyCode)); }); } - (void)flagsChanged:(NSEvent *)event { [super flagsChanged:event]; auto flags = [event modifierFlags]; App::I->ui_task_async([flags] { static bool pressed_shift = false; if (pressed_shift != (flags & NSShiftKeyMask)) { pressed_shift = (flags & NSShiftKeyMask); pressed_shift ? App::I->key_down(kKey::KeyShift) : App::I->key_up(kKey::KeyShift); } static bool pressed_alt = false; if (pressed_alt != (flags & NSAlternateKeyMask)) { pressed_alt = (flags & NSAlternateKeyMask); pressed_alt ? App::I->key_down(kKey::KeyAlt) : App::I->key_up(kKey::KeyAlt); } static bool pressed_cmd = false; if (pressed_cmd != (flags & NSCommandKeyMask)) { pressed_cmd = (flags & NSCommandKeyMask); pressed_cmd ? App::I->key_down(kKey::KeyCtrl) : App::I->key_up(kKey::KeyCtrl); } }); } @end @implementation Window @end @implementation Controller - (void)windowWillClose:(NSNotification *)notification { [[NSApplication sharedApplication] terminate:nil]; } -(BOOL)windowShouldClose:(NSWindow *)sender { return App::I->request_close(); } @end @implementation AppOSX - (instancetype)init { self = [super init]; [self setActivationPolicy:NSApplicationActivationPolicyRegular]; // make it to the front [self setDelegate:self]; return self; } - (BOOL)application:(NSApplication *)sender openFile:(NSString *)filename { LOG("open file %s", [filename UTF8String]); if (App::I) { std::string path = [filename UTF8String]; App::I->ui_task_async([=] { App::I->open_document(path); }); } return YES; } - (void)applicationWillTerminate:(NSNotification *)notification { [view terminateGL]; [window close]; } - (void)applicationDidFinishLaunching:(NSNotification *)notification { [MSAppCenter start:@"1500ff4a-32eb-4076-be3b-5147e2d8e27e" withServices:@[ [MSAnalytics class], [MSCrashes class] ]]; App::I->initLog(); App::I->create(); NSRect r = NSMakeRect(0, 0, App::I->width, App::I->height); if (Settings::has("window-rect")) { glm::ivec4 wr = Settings::value("window-rect"); r.origin = CGPointMake(wr.x, wr.y); r.size = CGSizeMake(wr.z, wr.w); } view = [[View alloc] initWithFrame:r]; controller = [[Controller alloc] initWithWindow:window]; App::I->osx_view = view; float z = (float)window.backingScaleFactor; App::I->zoom = Settings::value_or("ui-scale", (z > 0.f) ? z : 1.f); auto style = NSTitledWindowMask|NSMiniaturizableWindowMask|NSResizableWindowMask|NSClosableWindowMask; window = [[Window alloc] initWithContentRect:r styleMask:style backing:NSBackingStoreBuffered defer:NO]; [window setDelegate:controller]; [window setTitle:[NSString stringWithUTF8String:g_window_title]]; [window center]; [window makeKeyAndOrderFront:controller]; [window setContentView:view]; [window setAcceptsMouseMovedEvents:true]; [window makeFirstResponder:view]; view->wnd = window; auto menubar = [NSMenu new]; auto appMenuItem = [NSMenuItem new]; [menubar addItem:appMenuItem]; [self setMainMenu:menubar]; auto appMenu = [NSMenu new]; auto appName = [[NSProcessInfo processInfo] processName]; auto quitTitle = [@"Quit " stringByAppendingString:appName]; auto quitMenuItem = [[NSMenuItem alloc] initWithTitle:quitTitle action:@selector(terminate:) keyEquivalent:@"q"]; [appMenu addItem:quitMenuItem]; [appMenuItem setSubmenu:appMenu]; LOG("app launched"); App::I->ui_thread_start(); } - (void)save_ui_state { Settings::set("window-rect", Serializer::IVec4({window.frame.origin.x, window.frame.origin.y, window.frame.size.width, window.frame.size.height})); } - (void)init_dirs { NSArray* paths = NSSearchPathForDirectoriesInDomains(NSPicturesDirectory, NSUserDomainMask, YES); NSString* docpath = [(NSString*)[paths objectAtIndex:0] stringByAppendingString:@"/PanoPainter"]; NSError* err = nil; if (![[NSFileManager defaultManager] createDirectoryAtPath:docpath withIntermediateDirectories:YES attributes:nil error:&err]) { LOG("error creating rec path: %s", [[err localizedDescription] cStringUsingEncoding:NSASCIIStringEncoding]); } App::I->data_path = [docpath cStringUsingEncoding:NSASCIIStringEncoding]; NSString* recpath = [docpath stringByAppendingString:@"/rec"]; App::I->rec_path = [recpath cStringUsingEncoding:NSASCIIStringEncoding]; if (![[NSFileManager defaultManager] createDirectoryAtPath:recpath withIntermediateDirectories:YES attributes:nil error:&err]) { LOG("error creating rec path: %s", [[err localizedDescription] cStringUsingEncoding:NSASCIIStringEncoding]); } // brushes if (![[NSFileManager defaultManager] createDirectoryAtPath:[docpath stringByAppendingString:@"/brushes"] withIntermediateDirectories:YES attributes:nil error:&err]) { LOG("error creating brushes path: %s", [[err localizedDescription] cStringUsingEncoding:NSASCIIStringEncoding]); } if (![[NSFileManager defaultManager] createDirectoryAtPath:[docpath stringByAppendingString:@"/brushes/thumbs"] withIntermediateDirectories:YES attributes:nil error:&err]) { LOG("error creating brushes thumbs path: %s", [[err localizedDescription] cStringUsingEncoding:NSASCIIStringEncoding]); } // textures if (![[NSFileManager defaultManager] createDirectoryAtPath:[docpath stringByAppendingString:@"/patterns"] withIntermediateDirectories:YES attributes:nil error:&err]) { LOG("error creating patterns path: %s", [[err localizedDescription] cStringUsingEncoding:NSASCIIStringEncoding]); } if (![[NSFileManager defaultManager] createDirectoryAtPath:[docpath stringByAppendingString:@"/patterns/thumbs"] withIntermediateDirectories:YES attributes:nil error:&err]) { LOG("error creating patterns thumbs path: %s", [[err localizedDescription] cStringUsingEncoding:NSASCIIStringEncoding]); } // settings if (![[NSFileManager defaultManager] createDirectoryAtPath:[docpath stringByAppendingString:@"/settings"] withIntermediateDirectories:YES attributes:nil error:&err]) { LOG("error creating settings path: %s", [[err localizedDescription] cStringUsingEncoding:NSASCIIStringEncoding]); } } @end int main(int argc, const char * argv[]) { install_global_handlers(); App::I = new App; if (!App::I->check_license()) return 0; AppOSX* app = [AppOSX sharedApplication]; App::I->osx_app = app; [app run]; return 0; }