// // GameViewController.m // PanoPainter // // Created by Omar Mohamed Ali Mudhir on 07/05/17. // Copyright © 2017 Omar Mohamed Ali Mudhir. All rights reserved. // #include "pch.h" #import "GameViewController.h" #import #include "app.h" #include "settings.h" #import "objc_utils.h" #import std::mutex render_mutex; std::condition_variable render_cv; @interface GameImagePicker : UIImagePickerController { @public std::promise promise; @public std::function callback; } @end @implementation GameImagePicker @end @interface GameFilePicker : UIDocumentPickerViewController { @public std::promise promise; @public std::function callback; } @end @implementation GameFilePicker @end //std::map< bool pen_down = false; int t_count = 0; glm::vec2 t_pos; int lock_count = 0; NSThread* lock_thread; std::recursive_mutex lock_mutex; @implementation GameViewController - (std::string)clipboard_get_string { NSString* ns = [[UIPasteboard generalPasteboard] string]; 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()]; [[UIPasteboard generalPasteboard] setString:ns]; return true; } - (void)crash { AppDelegate* app = (AppDelegate*)[[UIApplication sharedApplication] delegate]; [app hockeyapp_crash]; } - (void)async_lock { lock_mutex.lock(); [EAGLContext setCurrentContext:context]; // GameView* view = (GameView*)self.view; // [view bindDrawable]; } - (void)async_unlock { lock_mutex.unlock(); } - (void)async_swap { [context presentRenderbuffer:GL_FRAMEBUFFER]; [glview display]; } - (void)share_file:(NSString *)file_path { NSURL *url = [NSURL fileURLWithPath:file_path]; NSArray *objectsToShare = @[url]; UIActivityViewController *controller = [[UIActivityViewController alloc] initWithActivityItems:objectsToShare applicationActivities:nil]; controller.excludedActivityTypes = @[]; controller.popoverPresentationController.sourceView = self.view; controller.popoverPresentationController.sourceRect = CGRectMake(self.view.bounds.size.width/2, self.view.bounds.size.height/4, 0, 0); // Present the controller [self presentViewController:controller animated:YES completion:nil]; } -(void)display_file:(std::string)filename { NSString* filePath = [NSString stringWithUTF8String:filename.c_str()]; NSURL *url = [NSURL fileURLWithPath:filePath]; UIDocumentInteractionController *popup = [UIDocumentInteractionController interactionControllerWithURL:url]; [popup setDelegate:self]; [popup presentPreviewAnimated:YES]; } -(UIViewController *)documentInteractionControllerViewControllerForPreview:(UIDocumentInteractionController *)controller { return self; } - (void)init_dirs { NSArray* paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString* docpath = (NSString*)[paths objectAtIndex:0]; App::I->data_path = [docpath cStringUsingEncoding:NSASCIIStringEncoding]; NSError* err = nil; 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]); } // tmp NSString* tmppath = [docpath stringByAppendingString:@"/tmp"]; App::I->tmp_path = [tmppath cStringUsingEncoding:NSASCIIStringEncoding]; if (![[NSFileManager defaultManager] createDirectoryAtPath:tmppath 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]); } // patterns 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]); } } - (void)pick_photo:(std::function) callback { GameImagePicker *picker = [[GameImagePicker alloc] init]; picker.delegate = self; picker.allowsEditing = NO; picker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary; picker->callback = callback; [self presentViewController:picker animated:YES completion:NULL]; } - (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info { NSString *chosenImage = [info[UIImagePickerControllerImageURL] path]; GameImagePicker* p = static_cast(picker); std::string path = [chosenImage cStringUsingEncoding:NSUTF8StringEncoding]; p->callback(path); [picker dismissViewControllerAnimated:YES completion:NULL]; } - (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker { [picker dismissViewControllerAnimated:YES completion:NULL]; } - (void)pick_file:(NSArray*)types then:(std::function)callback { // convert extension into UTIs from the system database NSMutableArray* UTIs = [NSMutableArray arrayWithCapacity:types.count]; for (NSString* ext : types) { NSString *typeForExt = (__bridge NSString*) UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)ext, NULL); [UTIs addObject:typeForExt]; } GameFilePicker *picker = [[GameFilePicker alloc] initWithDocumentTypes:UTIs inMode:UIDocumentPickerModeImport]; picker.delegate = self; picker->callback = callback; [self presentViewController:picker animated:YES completion:NULL]; } - (void)pick_file_save:(std::string)path { NSURL* url = [NSURL fileURLWithPath:[NSString stringWithUTF8String:path.c_str()]]; GameFilePicker *picker = [[GameFilePicker alloc] initWithURL:url inMode:UIDocumentPickerModeExportToService]; picker.delegate = self; [self presentViewController:picker animated:YES completion:NULL]; } -(void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentAtURL:(NSURL *)url { NSString *chosenImage = [url path]; GameFilePicker* p = static_cast(controller); if (p->callback) { std::string path = [chosenImage cStringUsingEncoding:NSUTF8StringEncoding]; p->callback(path); } [controller dismissViewControllerAnimated:YES completion:NULL]; } -(void)documentPickerWasCancelled:(UIDocumentPickerViewController *)controller { [controller dismissViewControllerAnimated:YES completion:NULL]; } - (void)insertText:(NSString *)text { if (const char* cstr = [text cStringUsingEncoding:NSISOLatin1StringEncoding]) { char c = cstr[0]; App::I->ui_task_async([=] { App::I->key_char(c); }); } NSLog(@"%@", text); // Do something with the typed character } - (void)deleteBackward { App::I->ui_task_async([=] { App::I->key_char('\b'); }); // Handle the delete key } - (BOOL)hasText { // Return whether there's any text present return YES; } - (BOOL)canBecomeFirstResponder { return input_enabled; } -(void)show_keyboard { input_enabled = YES; [self becomeFirstResponder]; } -(void)hide_keyboard { input_enabled = NO; [self resignFirstResponder]; } // Call this method somewhere in your view controller setup code. - (void)registerForKeyboardNotifications { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillBeShown:) name:UIKeyboardWillShowNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillBeHidden:) name:UIKeyboardWillHideNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWasShown:) name:UIKeyboardDidShowNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWasHidden:) name:UIKeyboardDidHideNotification object:nil]; } - (void)unregisterForKeyboardNotifications { [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillShowNotification object:nil]; [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillHideNotification object:nil]; [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardDidShowNotification object:nil]; [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardDidHideNotification object:nil]; } - (void)keyboardWillBeShown:(NSNotification*)aNotification { App::I->redraw = true; App::I->animate = true; } - (void)keyboardWasHidden:(NSNotification*)aNotification { App::I->redraw = true; App::I->animate = false; //[self unregisterForKeyboardNotifications]; } // Called when the UIKeyboardDidShowNotification is sent. - (void)keyboardWasShown:(NSNotification*)aNotification { NSDictionary* info = [aNotification userInfo]; CGSize kbSize = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue].size; GameView *view = (GameView *)self.view; //CGRect frame = view.frame; CGRect frame = [[UIScreen mainScreen] bounds]; frame.size.height -= kbSize.height; view.frame = frame; App::I->ui_task_async([size=frame.size, f=self.view.contentScaleFactor] { App::I->resize(size.width * f, size.height * f); }); App::I->animate = false; } // Called when the UIKeyboardWillHideNotification is sent - (void)keyboardWillBeHidden:(NSNotification*)aNotification { CGRect frame = [[UIScreen mainScreen] bounds]; self.view.frame = frame; App::I->ui_task_async([size=frame.size, f=self.view.contentScaleFactor] { App::I->resize(size.width * f, size.height * f); }); App::I->animate = true; } - (void)reset_touch { App::I->ui_task_async([=] { if (t_count == 2) App::I->gesture_end(); else App::I->mouse_cancel(0); }); t_count = 0; } float get_force(UITouch* t) { //glm::pow(t.force / t.maximumPossibleForce, 0.5); return glm::clamp(t.force / 1.0, 0.0, 1.0); } std::set ignored_touch; std::map touch_start; int max_touch_count = 0; bool is_tap = true; - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { max_touch_count = (int)[[event allTouches] count]; UITouch *touch = [[event allTouches] anyObject]; CGPoint touchLocation = [touch locationInView:self.view]; float scale = self.view.contentScaleFactor; float force = get_force(touch); AppDelegate* app = (AppDelegate*)[[UIApplication sharedApplication] delegate]; if ([app sonarpen_present]) force = [app sonarpen_pressure]; kEventSource source = ((touch.type == UITouchType::UITouchTypeStylus) || [app sonarpen_present]) ? kEventSource::Stylus : kEventSource::Touch; // apple pencil if (touch.type == UITouchType::UITouchTypeStylus) { pen_down = true; is_tap = false; } else { is_tap = true; touch_start.clear(); } App::I->ui_task_async([touchLocation, scale, f=force, source] { App::I->mouse_down(0, touchLocation.x * scale, touchLocation.y * scale, f, source, 0); }); t_count = 1; t_pos = {touchLocation.x * scale, touchLocation.y * scale}; } - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { is_tap = false; int n = (int)[[event allTouches] count]; float scale = self.view.contentScaleFactor; max_touch_count = std::max(max_touch_count, n); UITouch* stylus_touch = nullptr; std::vector valid_touches; for (int i = 0; i < n; i++) { auto t = [[[event allTouches] allObjects] objectAtIndex:i]; if (t.type == UITouchType::UITouchTypeStylus) stylus_touch = t; auto it = ignored_touch.find(t); if (it == ignored_touch.end()) { valid_touches.push_back(t); if (touch_start.find(t) == touch_start.end()) { auto loc = [t locationInView:self.view]; touch_start[t] = { loc.x, loc.y }; } } else { NSLog(@"ignored moved"); } } n = (int)valid_touches.size(); if (n == 0) return; UITouch* t0 = nullptr; UITouch* t1 = nullptr; t0 = valid_touches[0]; CGPoint tl0 = [t0 locationInView:self.view]; glm::vec2 p0 = glm::vec2(tl0.x * scale, tl0.y * scale); glm::vec2 p1; if (n > 1) { t1 = valid_touches[1]; CGPoint tl1 = [t1 locationInView:self.view]; p1 = glm::vec2(tl1.x * scale, tl1.y * scale); } if (pen_down) { if (stylus_touch) { auto loc = [stylus_touch locationInView:self.view]; auto p = glm::vec2(loc.x, loc.y) * scale; App::I->ui_task_async([tt=stylus_touch.type,p,f=get_force(stylus_touch)] { App::I->mouse_move(p.x, p.y, f, kEventSource::Stylus, 0); }); } } else if (stylus_touch) { pen_down = true; auto loc = [stylus_touch locationInView:self.view]; auto p = glm::vec2(loc.x, loc.y) * scale; float f = get_force(stylus_touch); App::I->ui_task_async([tc=t_count, p, f] { if (tc == 2) App::I->gesture_end(); else if (tc == 1) App::I->mouse_cancel(0); App::I->mouse_down(0, p.x, p.y, f, kEventSource::Stylus, 0); App::I->mouse_move(p.x, p.y, f, kEventSource::Stylus, 0); }); t_count = 0; } else if (n == 2) { App::I->ui_task_async([c=t_count, p0, p1] { if (c == 1) { App::I->mouse_cancel(0); App::I->gesture_start(p0, p1); } else App::I->gesture_move(p0, p1); }); t_count = 2; } else if (n == 1) { UITouch *touch = valid_touches[0]; CGPoint touchLocation = [touch locationInView:self.view]; float force = touch.type == UITouchType::UITouchTypeStylus ? get_force(touch) : 1.0f; AppDelegate* app = (AppDelegate*)[[UIApplication sharedApplication] delegate]; if ([app sonarpen_present]) force = [app sonarpen_pressure]; if (t_count == 2) { //App::I->gesture_end(); //App::I->mouse_down(0, touchLocation.x * scale, touchLocation.y * scale, force); } else { kEventSource source = ((touch.type == UITouchType::UITouchTypeStylus) || [app sonarpen_present]) ? kEventSource::Stylus : kEventSource::Touch; App::I->ui_task_async([touchLocation, scale, source, force] { App::I->mouse_move(touchLocation.x * scale, touchLocation.y * scale, force, source, 0); }); } } t_pos = {tl0.x * scale, tl0.y * scale}; } -(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { static NSTimeInterval last_time = 0; static int tap_count = 1; int n = (int)[[event allTouches] count]; UITouch *touch = [[event allTouches] anyObject]; CGPoint touchLocation = [touch locationInView:self.view]; float scale = self.view.contentScaleFactor; if (touch.timestamp - last_time < 0.2) tap_count++; else tap_count = 1; last_time = touch.timestamp; glm::vec2 center(0); float max_dist = 0; for (UITouch* t : [event allTouches]) { auto loc = [t locationInView:self.view]; center += glm::vec2(loc.x, loc.y); if (touch_start.find(t) != touch_start.end()) { max_dist = std::max(max_dist, glm::distance(touch_start[t], { loc.x, loc.y })); } } center /= (float)n; //LOG("max touches (%d-%d) (max %d) dist %f", // n, (int)touch_start.size(), max_touch_count, (int)max_dist); if (ignored_touch.count(touch)) { ignored_touch.erase(touch); NSLog(@"remove ignored"); } kEventSource source = touch.type == UITouchType::UITouchTypeStylus ? kEventSource::Stylus : kEventSource::Touch; pen_down = false; App::I->ui_task_async([tc=t_count, touchLocation, scale, source, n, center, max_dist] { if (tc == 2) App::I->gesture_end(); else App::I->mouse_up(0, touchLocation.x * scale, touchLocation.y * scale, source, 0); if (is_tap || max_dist < 10) App::I->touch_tap(center, n == 1 ? max_touch_count : n, tap_count); }); t_count = 0; t_pos = {touchLocation.x * scale, touchLocation.y * scale}; } - (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id)coordinator { App::I->ui_task_async([f=self.view.contentScaleFactor, size] { App::I->resize(size.width * f, size.height * f); }); } - (void)viewDidAppear:(BOOL)animated { App::I->redraw = true; [self resignFirstResponder]; [self registerForKeyboardNotifications]; } - (void)viewDidLoad { [super viewDidLoad]; input_enabled = NO; App::I = new App; App::I->ios_view = self; App::I->ios_app = (AppDelegate*)[[UIApplication sharedApplication] delegate]; App::I->initLog(); //self.preferredFramesPerSecond = 60; context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3]; if (!context) { NSLog(@"Failed to create ES context"); } GameView *view = (GameView *)self.view; view.context = context; view.drawableDepthFormat = GLKViewDrawableDepthFormat24; view.enableSetNeedsDisplay = NO; glview = view; App::I->width = view.frame.size.width * view.contentScaleFactor; App::I->height = view.frame.size.height * view.contentScaleFactor; App::I->zoom = Settings::value_or("ui-scale", (float)view.contentScaleFactor); App::I->render_thread_start(); App::I->ui_thread_start(); } - (BOOL)prefersStatusBarHidden { return YES; } @end @implementation GameView @end