Files
panopainter/PanoPainter-OSX/main.cpp
2019-09-07 11:39:45 +02:00

592 lines
19 KiB
C++

#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 <CoreFoundation/CoreFoundation.h>
#include <Cocoa/Cocoa.h>
#include <CoreVideo/CoreVideo.h>
#include <OpenGL/OpenGL.h>
#import "objc_utils.h"
#include <deque>
#include <chrono>
@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<NSString*>*)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<NSString*>*)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<Serializer::IVec4>("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<Serializer::Float>("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;
}