From 0712ed904b7cb6385e840eb0d9682d69a7306bda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Thu, 24 Dec 2015 20:49:17 -0800 Subject: [PATCH] [osx] Make osxapp document-based Allows for multiple map windows open at a time. --- platform/osx/app/AppDelegate.h | 1 + platform/osx/app/AppDelegate.m | 540 +-------------------------- platform/osx/app/Info.plist | 15 + platform/osx/app/MainMenu.xib | 120 +----- platform/osx/app/MapDocument.h | 14 + platform/osx/app/MapDocument.m | 568 +++++++++++++++++++++++++++++ platform/osx/app/MapDocument.xib | 129 +++++++ platform/osx/app/mapboxgl-app.gypi | 3 + 8 files changed, 746 insertions(+), 644 deletions(-) create mode 100644 platform/osx/app/MapDocument.h create mode 100644 platform/osx/app/MapDocument.m create mode 100644 platform/osx/app/MapDocument.xib diff --git a/platform/osx/app/AppDelegate.h b/platform/osx/app/AppDelegate.h index 69b6e0f13..9f76ee76f 100644 --- a/platform/osx/app/AppDelegate.h +++ b/platform/osx/app/AppDelegate.h @@ -2,6 +2,7 @@ @interface AppDelegate : NSObject +@property (weak) IBOutlet NSWindow *preferencesWindow; @end diff --git a/platform/osx/app/AppDelegate.m b/platform/osx/app/AppDelegate.m index 6aaf96535..76c522207 100644 --- a/platform/osx/app/AppDelegate.m +++ b/platform/osx/app/AppDelegate.m @@ -1,40 +1,18 @@ #import "AppDelegate.h" -#import "DroppedPinAnnotation.h" #import "LocationCoordinate2DTransformer.h" +#import "MapDocument.h" #import "NSValue+Additions.h" #import static NSString * const MGLMapboxAccessTokenDefaultsKey = @"MGLMapboxAccessToken"; -static NSString * const MGLDroppedPinAnnotationImageIdentifier = @"dropped"; -static const CLLocationCoordinate2D WorldTourDestinations[] = { - { .latitude = 38.9131982, .longitude = -77.0325453144239 }, - { .latitude = 37.7757368, .longitude = -122.4135302 }, - { .latitude = 12.9810816, .longitude = 77.6368034 }, - { .latitude = -13.15589555, .longitude = -74.2178961777998 }, -}; - -@interface AppDelegate () - -@property (weak) IBOutlet NSWindow *window; -@property (weak) IBOutlet MGLMapView *mapView; -@property (weak) IBOutlet NSMenu *mapViewContextMenu; - -@property (weak) IBOutlet NSWindow *preferencesWindow; +@interface AppDelegate () @end -@implementation AppDelegate { - NSPoint _mouseLocationForMapViewContextMenu; - NSUInteger _droppedPinCounter; - NSNumberFormatter *_spellOutNumberFormatter; - - BOOL _showsToolTipsOnDroppedPins; - BOOL _randomizesCursorsOnDroppedPins; - BOOL _isTouringWorld; -} +@implementation AppDelegate #pragma mark Lifecycle @@ -85,16 +63,6 @@ static const CLLocationCoordinate2D WorldTourDestinations[] = { [alert runModal]; [self showPreferences:nil]; } - - NSURL *savedURL = [[NSUserDefaults standardUserDefaults] URLForKey:@"MBXCurrentStyleURL"]; - if (savedURL) { - self.mapView.styleURL = savedURL; - } - - _spellOutNumberFormatter = [[NSNumberFormatter alloc] init]; - - NSPressGestureRecognizer *pressGestureRecognizer = [[NSPressGestureRecognizer alloc] initWithTarget:self action:@selector(handlePressGesture:)]; - [self.mapView addGestureRecognizer:pressGestureRecognizer]; } - (void)applicationWillTerminate:(NSNotification *)notification { @@ -106,13 +74,20 @@ static const CLLocationCoordinate2D WorldTourDestinations[] = { NSString *accessToken = [userDefaults stringForKey:MGLMapboxAccessTokenDefaultsKey]; if (![accessToken isEqualToString:[MGLAccountManager accessToken]]) { [MGLAccountManager setAccessToken:accessToken]; - [self reload:self]; + [self.mainDocument reload:self]; } } +- (MapDocument *)mainDocument { + NSDocument *mainDocument = [NSApp mainWindow].windowController.document; + return [mainDocument isKindOfClass:[MapDocument class]] ? (MapDocument *)mainDocument : nil; +} + #pragma mark Services - (void)handleGetURLEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent { + [[NSDocumentController sharedDocumentController] newDocument:self]; + NSURL *url = [NSURL URLWithString:[event paramDescriptorForKeyword:keyDirectObject].stringValue]; NS_MUTABLE_DICTIONARY_OF(NSString *, NSString *) *params = [[NSMutableDictionary alloc] init]; for (NSString *param in [url.query componentsSeparatedByString:@"&"]) { @@ -126,220 +101,22 @@ static const CLLocationCoordinate2D WorldTourDestinations[] = { if (centerString) { NSArray *coordinateValues = [centerString componentsSeparatedByString:@","]; if (coordinateValues.count == 2) { - self.mapView.centerCoordinate = CLLocationCoordinate2DMake([coordinateValues[0] doubleValue], - [coordinateValues[1] doubleValue]); + self.mainDocument.mapView.centerCoordinate = CLLocationCoordinate2DMake([coordinateValues[0] doubleValue], + [coordinateValues[1] doubleValue]); } } NSString *zoomLevelString = params[@"zoom"]; if (zoomLevelString.length) { - self.mapView.zoomLevel = zoomLevelString.doubleValue; + self.mainDocument.mapView.zoomLevel = zoomLevelString.doubleValue; } NSString *directionString = params[@"bearing"]; if (directionString.length) { - self.mapView.direction = directionString.doubleValue; + self.mainDocument.mapView.direction = directionString.doubleValue; } } -- (IBAction)showShareMenu:(id)sender { - NSSharingServicePicker *picker = [[NSSharingServicePicker alloc] initWithItems:@[self.shareURL]]; - picker.delegate = self; - [picker showRelativeToRect:[sender bounds] ofView:sender preferredEdge:NSMinYEdge]; -} - -- (NSURL *)shareURL { - NSArray *components = self.mapView.styleURL.pathComponents; - CLLocationCoordinate2D centerCoordinate = self.mapView.centerCoordinate; - return [NSURL URLWithString: - [NSString stringWithFormat:@"https://api.mapbox.com/styles/v1/%@/%@.html?access_token=%@#%.2f/%.5f/%.5f/%.f", - components[1], components[2], [MGLAccountManager accessToken], - self.mapView.zoomLevel, centerCoordinate.latitude, centerCoordinate.longitude, self.mapView.direction]]; -} - -#pragma mark View methods - -- (IBAction)setStyle:(id)sender { - NSInteger tag; - if ([sender isKindOfClass:[NSMenuItem class]]) { - tag = [sender tag]; - } else if ([sender isKindOfClass:[NSPopUpButton class]]) { - tag = [sender selectedTag]; - } - NSURL *styleURL; - switch (tag) { - case 1: - styleURL = [MGLStyle streetsStyleURL]; - break; - case 2: - styleURL = [MGLStyle emeraldStyleURL]; - break; - case 3: - styleURL = [MGLStyle lightStyleURL]; - break; - case 4: - styleURL = [MGLStyle darkStyleURL]; - break; - case 5: - styleURL = [MGLStyle satelliteStyleURL]; - break; - case 6: - styleURL = [MGLStyle hybridStyleURL]; - break; - default: - NSAssert(NO, @"Cannot set style from control with tag %li", (long)tag); - break; - } - self.mapView.styleURL = styleURL; - [[NSUserDefaults standardUserDefaults] setURL:self.mapView.styleURL forKey:@"MBXCurrentStyleURL"]; - [self.window.toolbar validateVisibleItems]; -} - -- (IBAction)chooseCustomStyle:(id)sender { - NSAlert *alert = [[NSAlert alloc] init]; - alert.messageText = @"Apply custom style"; - alert.informativeText = @"Enter the URL to a JSON file that conforms to the Mapbox GL style specification, such as a style designed in Mapbox Studio:"; - NSTextField *textField = [[NSTextField alloc] initWithFrame:NSZeroRect]; - [textField sizeToFit]; - NSRect textFieldFrame = textField.frame; - textFieldFrame.size.width = 300; - textField.frame = textFieldFrame; - NSURL *savedURL = [[NSUserDefaults standardUserDefaults] URLForKey:@"MBXCustomStyleURL"]; - if (savedURL) { - textField.stringValue = savedURL.absoluteString; - } - alert.accessoryView = textField; - [alert addButtonWithTitle:@"Apply"]; - [alert addButtonWithTitle:@"Cancel"]; - if ([alert runModal] == NSAlertFirstButtonReturn) { - self.mapView.styleURL = [NSURL URLWithString:textField.stringValue]; - [[NSUserDefaults standardUserDefaults] setURL:self.mapView.styleURL forKey:@"MBXCustomStyleURL"]; - [[NSUserDefaults standardUserDefaults] setURL:self.mapView.styleURL forKey:@"MBXCurrentStyleURL"]; - [self.window.toolbar validateVisibleItems]; - } -} - -- (IBAction)zoomIn:(id)sender { - [self.mapView setZoomLevel:self.mapView.zoomLevel + 1 animated:YES]; -} - -- (IBAction)zoomOut:(id)sender { - [self.mapView setZoomLevel:self.mapView.zoomLevel - 1 animated:YES]; -} - -- (IBAction)snapToNorth:(id)sender { - [self.mapView setDirection:0 animated:YES]; -} - -- (IBAction)reload:(id)sender { - [self.mapView reloadStyle:sender]; -} - -#pragma mark Debug methods - -- (IBAction)toggleTileBoundaries:(id)sender { - self.mapView.debugMask ^= MGLMapDebugTileBoundariesMask; -} - -- (IBAction)toggleTileInfo:(id)sender { - self.mapView.debugMask ^= MGLMapDebugTileInfoMask; -} - -- (IBAction)toggleTileTimestamps:(id)sender { - self.mapView.debugMask ^= MGLMapDebugTimestampsMask; -} - -- (IBAction)toggleCollisionBoxes:(id)sender { - self.mapView.debugMask ^= MGLMapDebugCollisionBoxesMask; -} - -- (IBAction)toggleShowsToolTipsOnDroppedPins:(id)sender { - _showsToolTipsOnDroppedPins = !_showsToolTipsOnDroppedPins; -} - -- (IBAction)toggleRandomizesCursorsOnDroppedPins:(id)sender { - _randomizesCursorsOnDroppedPins = !_randomizesCursorsOnDroppedPins; -} - -- (IBAction)dropManyPins:(id)sender { - [self.mapView removeAnnotations:self.mapView.annotations]; - - NSRect bounds = self.mapView.bounds; - NSMutableArray *annotations = [NSMutableArray array]; - for (CGFloat x = NSMinX(bounds); x < NSMaxX(bounds); x += arc4random_uniform(50)) { - for (CGFloat y = NSMaxY(bounds); y >= NSMinY(bounds); y -= arc4random_uniform(100)) { - [annotations addObject:[self pinAtPoint:NSMakePoint(x, y)]]; - } - } - - [NSTimer scheduledTimerWithTimeInterval:1/60 - target:self - selector:@selector(dropOneOfManyPins:) - userInfo:annotations - repeats:YES]; -} - -- (void)dropOneOfManyPins:(NSTimer *)timer { - NSMutableArray *annotations = timer.userInfo; - NSUInteger numberOfAnnotationsToAdd = 50; - if (annotations.count < numberOfAnnotationsToAdd) { - numberOfAnnotationsToAdd = annotations.count; - } - NSArray *annotationsToAdd = [annotations subarrayWithRange: - NSMakeRange(0, numberOfAnnotationsToAdd)]; - [self.mapView addAnnotations:annotationsToAdd]; - [annotations removeObjectsInRange:NSMakeRange(0, numberOfAnnotationsToAdd)]; - if (!annotations.count) { - [timer invalidate]; - } -} - -- (IBAction)removeAllPins:(id)sender { - [self.mapView removeAnnotations:self.mapView.annotations]; -} - -- (IBAction)startWorldTour:(id)sender { - _isTouringWorld = YES; - - [self removeAllPins:sender]; - NSUInteger numberOfAnnotations = sizeof(WorldTourDestinations) / sizeof(WorldTourDestinations[0]); - NSMutableArray *annotations = [NSMutableArray arrayWithCapacity:numberOfAnnotations]; - for (NSUInteger i = 0; i < numberOfAnnotations; i++) { - MGLPointAnnotation *annotation = [[MGLPointAnnotation alloc] init]; - annotation.coordinate = WorldTourDestinations[i]; - [annotations addObject:annotation]; - } - [self.mapView addAnnotations:annotations]; - [self continueWorldTourWithRemainingAnnotations:annotations]; -} - -- (void)continueWorldTourWithRemainingAnnotations:(NS_MUTABLE_ARRAY_OF(MGLPointAnnotation *) *)annotations { - MGLPointAnnotation *nextAnnotation = annotations.firstObject; - if (!nextAnnotation || !_isTouringWorld) { - _isTouringWorld = NO; - return; - } - - [annotations removeObjectAtIndex:0]; - MGLMapCamera *camera = [MGLMapCamera cameraLookingAtCenterCoordinate:nextAnnotation.coordinate - fromDistance:0 - pitch:arc4random_uniform(60) - heading:arc4random_uniform(360)]; - __weak AppDelegate *weakSelf = self; - [self.mapView flyToCamera:camera completionHandler:^{ - AppDelegate *strongSelf = weakSelf; - [strongSelf performSelector:@selector(continueWorldTourWithRemainingAnnotations:) - withObject:annotations - afterDelay:2]; - }]; -} - -- (IBAction)stopWorldTour:(id)sender { - _isTouringWorld = NO; - // Any programmatic viewpoint change cancels outstanding animations. - self.mapView.camera = self.mapView.camera; -} - #pragma mark Help methods - (IBAction)showShortcuts:(id)sender { @@ -355,13 +132,6 @@ static const CLLocationCoordinate2D WorldTourDestinations[] = { [alert runModal]; } -- (IBAction)giveFeedback:(id)sender { - CLLocationCoordinate2D centerCoordinate = self.mapView.centerCoordinate; - NSURL *feedbackURL = [NSURL URLWithString:[NSString stringWithFormat:@"https://www.mapbox.com/map-feedback/#/%.5f/%.5f/%.0f", - centerCoordinate.longitude, centerCoordinate.latitude, round(self.mapView.zoomLevel)]]; - [[NSWorkspace sharedWorkspace] openURL:feedbackURL]; -} - - (IBAction)showPreferences:(id)sender { [self.preferencesWindow makeKeyAndOrderFront:sender]; } @@ -370,296 +140,16 @@ static const CLLocationCoordinate2D WorldTourDestinations[] = { [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"https://www.mapbox.com/studio/account/tokens/"]]; } -#pragma mark Mouse events - -- (void)handlePressGesture:(NSPressGestureRecognizer *)gestureRecognizer { - if (gestureRecognizer.state == NSGestureRecognizerStateBegan) { - NSPoint location = [gestureRecognizer locationInView:self.mapView]; - [self dropPinAtPoint:location]; - } -} - -- (IBAction)dropPin:(NSMenuItem *)sender { - [self dropPinAtPoint:_mouseLocationForMapViewContextMenu]; -} - -- (void)dropPinAtPoint:(NSPoint)point { - DroppedPinAnnotation *annotation = [self pinAtPoint:point]; - [self.mapView addAnnotation:annotation]; - [self.mapView selectAnnotation:annotation]; -} - -- (DroppedPinAnnotation *)pinAtPoint:(NSPoint)point { - DroppedPinAnnotation *annotation = [[DroppedPinAnnotation alloc] init]; - annotation.coordinate = [self.mapView convertPoint:point toCoordinateFromView:self.mapView]; - annotation.title = @"Dropped Pin"; - _spellOutNumberFormatter.numberStyle = NSNumberFormatterSpellOutStyle; - if (_showsToolTipsOnDroppedPins) { - NSString *formattedNumber = [_spellOutNumberFormatter stringFromNumber:@(++_droppedPinCounter)]; - annotation.toolTip = formattedNumber; - } - return annotation; -} - -- (IBAction)removePin:(NSMenuItem *)sender { - [self removePinAtPoint:_mouseLocationForMapViewContextMenu]; -} - -- (void)removePinAtPoint:(NSPoint)point { - [self.mapView removeAnnotation:[self.mapView annotationAtPoint:point]]; -} - #pragma mark User interface validation - (BOOL)validateMenuItem:(NSMenuItem *)menuItem { - if (menuItem.action == @selector(setStyle:)) { - NSURL *styleURL = self.mapView.styleURL; - NSCellStateValue state; - switch (menuItem.tag) { - case 1: - state = [styleURL isEqual:[MGLStyle streetsStyleURL]]; - break; - case 2: - state = [styleURL isEqual:[MGLStyle emeraldStyleURL]]; - break; - case 3: - state = [styleURL isEqual:[MGLStyle lightStyleURL]]; - break; - case 4: - state = [styleURL isEqual:[MGLStyle darkStyleURL]]; - break; - case 5: - state = [styleURL isEqual:[MGLStyle satelliteStyleURL]]; - break; - case 6: - state = [styleURL isEqual:[MGLStyle hybridStyleURL]]; - break; - default: - return NO; - } - menuItem.state = state; - return YES; - } - if (menuItem.action == @selector(chooseCustomStyle:)) { - menuItem.state = self.indexOfStyleInToolbarItem == NSNotFound; - return YES; - } - if (menuItem.action == @selector(zoomIn:)) { - return self.mapView.zoomLevel < self.mapView.maximumZoomLevel; - } - if (menuItem.action == @selector(zoomOut:)) { - return self.mapView.zoomLevel > self.mapView.minimumZoomLevel; - } - if (menuItem.action == @selector(snapToNorth:)) { - return self.mapView.direction != 0; - } - if (menuItem.action == @selector(reload:)) { - return YES; - } - if (menuItem.action == @selector(dropPin:)) { - BOOL isOverAnnotation = [self.mapView annotationAtPoint:_mouseLocationForMapViewContextMenu]; - menuItem.hidden = isOverAnnotation; - return YES; - } - if (menuItem.action == @selector(removePin:)) { - BOOL isOverAnnotation = [self.mapView annotationAtPoint:_mouseLocationForMapViewContextMenu]; - menuItem.hidden = !isOverAnnotation; - return YES; - } - if (menuItem.action == @selector(toggleTileBoundaries:)) { - BOOL isShown = self.mapView.debugMask & MGLMapDebugTileBoundariesMask; - menuItem.title = isShown ? @"Hide Tile Boundaries" : @"Show Tile Boundaries"; - return YES; - } - if (menuItem.action == @selector(toggleTileInfo:)) { - BOOL isShown = self.mapView.debugMask & MGLMapDebugTileInfoMask; - menuItem.title = isShown ? @"Hide Tile Info" : @"Show Tile Info"; - return YES; - } - if (menuItem.action == @selector(toggleTileTimestamps:)) { - BOOL isShown = self.mapView.debugMask & MGLMapDebugTimestampsMask; - menuItem.title = isShown ? @"Hide Tile Timestamps" : @"Show Tile Timestamps"; - return YES; - } - if (menuItem.action == @selector(toggleCollisionBoxes:)) { - BOOL isShown = self.mapView.debugMask & MGLMapDebugCollisionBoxesMask; - menuItem.title = isShown ? @"Hide Collision Boxes" : @"Show Collision Boxes"; - return YES; - } - if (menuItem.action == @selector(toggleShowsToolTipsOnDroppedPins:)) { - BOOL isShown = _showsToolTipsOnDroppedPins; - menuItem.title = isShown ? @"Hide Tooltips on Dropped Pins" : @"Show Tooltips on Dropped Pins"; - return YES; - } - if (menuItem.action == @selector(toggleRandomizesCursorsOnDroppedPins:)) { - BOOL isRandom = _randomizesCursorsOnDroppedPins; - menuItem.title = isRandom ? @"Use Default Cursor for Dropped Pins" : @"Use Random Cursors for Dropped Pins"; - return _showsToolTipsOnDroppedPins; - } - if (menuItem.action == @selector(dropManyPins:)) { - return YES; - } - if (menuItem.action == @selector(removeAllPins:)) { - return self.mapView.annotations.count; - } - if (menuItem.action == @selector(startWorldTour:)) { - return !_isTouringWorld; - } - if (menuItem.action == @selector(stopWorldTour:)) { - return _isTouringWorld; - } if (menuItem.action == @selector(showShortcuts:)) { return YES; } - if (menuItem.action == @selector(giveFeedback:)) { - return YES; - } if (menuItem.action == @selector(showPreferences:)) { return YES; } return NO; } -- (NSUInteger)indexOfStyleInToolbarItem { - if (![MGLAccountManager accessToken]) { - return NSNotFound; - } - - NSArray *styleURLs = @[ - [MGLStyle streetsStyleURL], - [MGLStyle emeraldStyleURL], - [MGLStyle lightStyleURL], - [MGLStyle darkStyleURL], - [MGLStyle satelliteStyleURL], - [MGLStyle hybridStyleURL], - ]; - return [styleURLs indexOfObject:self.mapView.styleURL]; -} - -- (BOOL)validateToolbarItem:(NSToolbarItem *)toolbarItem { - if (!self.mapView) { - return NO; - } - - if (toolbarItem.action == @selector(showShareMenu:)) { - [(NSButton *)toolbarItem.view sendActionOn:NSLeftMouseDownMask]; - if (![MGLAccountManager accessToken]) { - return NO; - } - NSURL *styleURL = self.mapView.styleURL; - return ([styleURL.scheme isEqualToString:@"mapbox"] - && [styleURL.pathComponents.firstObject isEqualToString:@"styles"]); - } - if (toolbarItem.action == @selector(setStyle:)) { - NSPopUpButton *popUpButton = (NSPopUpButton *)toolbarItem.view; - NSUInteger index = self.indexOfStyleInToolbarItem; - if (index == NSNotFound) { - [popUpButton addItemWithTitle:@"Custom"]; - index = [popUpButton numberOfItems] - 1; - } - [popUpButton selectItemAtIndex:index]; - } - return NO; -} - -#pragma mark NSApplicationDelegate methods - -- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender { - return YES; -} - -#pragma mark NSSharingServicePickerDelegate methods - -- (NS_ARRAY_OF(NSSharingService *) *)sharingServicePicker:(NSSharingServicePicker *)sharingServicePicker sharingServicesForItems:(NSArray *)items proposedSharingServices:(NS_ARRAY_OF(NSSharingService *) *)proposedServices { - NSURL *shareURL = self.shareURL; - NSURL *browserURL = [[NSWorkspace sharedWorkspace] URLForApplicationToOpenURL:shareURL]; - NSImage *browserIcon = [[NSWorkspace sharedWorkspace] iconForFile:browserURL.path]; - NSString *browserName = [[NSFileManager defaultManager] displayNameAtPath:browserURL.path]; - NSString *browserServiceName = [NSString stringWithFormat:@"Open in %@", browserName]; - - NSSharingService *browserService = [[NSSharingService alloc] initWithTitle:browserServiceName - image:browserIcon - alternateImage:nil - handler:^{ - [[NSWorkspace sharedWorkspace] openURL:self.shareURL]; - }]; - - NSMutableArray *sharingServices = [proposedServices mutableCopy]; - [sharingServices insertObject:browserService atIndex:0]; - return sharingServices; -} - -#pragma mark NSMenuDelegate methods - -- (void)menuWillOpen:(NSMenu *)menu { - if (menu == self.mapViewContextMenu) { - _mouseLocationForMapViewContextMenu = [self.window.contentView convertPoint:self.window.mouseLocationOutsideOfEventStream - toView:self.mapView]; - } -} - -#pragma mark MGLMapViewDelegate methods - -- (BOOL)mapView:(MGLMapView *)mapView annotationCanShowCallout:(id )annotation { - return YES; -} - -- (MGLAnnotationImage *)mapView:(MGLMapView *)mapView imageForAnnotation:(id )annotation { - MGLAnnotationImage *annotationImage = [self.mapView dequeueReusableAnnotationImageWithIdentifier:MGLDroppedPinAnnotationImageIdentifier]; - if (!annotationImage) { - NSString *imagePath = [[NSBundle bundleForClass:[MGLMapView class]] - pathForResource:@"default_marker" ofType:@"pdf"]; - NSImage *image = [[NSImage alloc] initWithContentsOfFile:imagePath]; - NSRect alignmentRect = image.alignmentRect; - alignmentRect.origin.y = NSMidY(alignmentRect); - alignmentRect.size.height /= 2; - image.alignmentRect = alignmentRect; - annotationImage = [MGLAnnotationImage annotationImageWithImage:image - reuseIdentifier:MGLDroppedPinAnnotationImageIdentifier]; - } - if (_randomizesCursorsOnDroppedPins) { - NSArray *cursors = @[ - [NSCursor IBeamCursor], - [NSCursor crosshairCursor], - [NSCursor pointingHandCursor], - [NSCursor disappearingItemCursor], - [NSCursor IBeamCursorForVerticalLayout], - [NSCursor operationNotAllowedCursor], - [NSCursor dragLinkCursor], - [NSCursor dragCopyCursor], - [NSCursor contextualMenuCursor], - ]; - annotationImage.cursor = cursors[arc4random_uniform(cursors.count)]; - } else { - annotationImage.cursor = nil; - } - return annotationImage; -} - -- (void)mapView:(MGLMapView *)mapView didSelectAnnotation:(id )annotation { - if ([annotation isKindOfClass:[DroppedPinAnnotation class]]) { - DroppedPinAnnotation *droppedPin = annotation; - [droppedPin resume]; - } -} - -- (void)mapView:(MGLMapView *)mapView didDeselectAnnotation:(id )annotation { - if ([annotation isKindOfClass:[DroppedPinAnnotation class]]) { - DroppedPinAnnotation *droppedPin = annotation; - [droppedPin pause]; - } -} - -@end - -@interface ValidatedToolbarItem : NSToolbarItem - -@end - -@implementation ValidatedToolbarItem - -- (void)validate { - [(AppDelegate *)self.toolbar.delegate validateToolbarItem:self]; -} - @end diff --git a/platform/osx/app/Info.plist b/platform/osx/app/Info.plist index 118f2c1b1..0ae973842 100644 --- a/platform/osx/app/Info.plist +++ b/platform/osx/app/Info.plist @@ -4,6 +4,21 @@ CFBundleDevelopmentRegion English + CFBundleDocumentTypes + + + CFBundleTypeExtensions + + mbx + + CFBundleTypeName + Mapbox GL Map + CFBundleTypeRole + Editor + NSDocumentClass + MapDocument + + CFBundleExecutable ${EXECUTABLE_NAME} CFBundleGetInfoString diff --git a/platform/osx/app/MainMenu.xib b/platform/osx/app/MainMenu.xib index 3f270c23f..844af08f3 100644 --- a/platform/osx/app/MainMenu.xib +++ b/platform/osx/app/MainMenu.xib @@ -1,5 +1,5 @@ - + @@ -17,10 +17,7 @@ - - - @@ -544,100 +541,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - LocationCoordinate2DTransformer - - - - @@ -700,29 +603,8 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/platform/osx/app/MapDocument.h b/platform/osx/app/MapDocument.h new file mode 100644 index 000000000..86ad05e6e --- /dev/null +++ b/platform/osx/app/MapDocument.h @@ -0,0 +1,14 @@ +#import + +@class MGLMapView; + +@interface MapDocument : NSDocument + +@property (weak) IBOutlet MGLMapView *mapView; + +- (IBAction)setStyle:(id)sender; +- (IBAction)chooseCustomStyle:(id)sender; + +- (IBAction)reload:(id)sender; + +@end diff --git a/platform/osx/app/MapDocument.m b/platform/osx/app/MapDocument.m new file mode 100644 index 000000000..b7d16e5c4 --- /dev/null +++ b/platform/osx/app/MapDocument.m @@ -0,0 +1,568 @@ +#import "MapDocument.h" + +#import "DroppedPinAnnotation.h" + +#import + +static NSString * const MGLDroppedPinAnnotationImageIdentifier = @"dropped"; + +static const CLLocationCoordinate2D WorldTourDestinations[] = { + { .latitude = 38.9131982, .longitude = -77.0325453144239 }, + { .latitude = 37.7757368, .longitude = -122.4135302 }, + { .latitude = 12.9810816, .longitude = 77.6368034 }, + { .latitude = -13.15589555, .longitude = -74.2178961777998 }, +}; + +@interface MapDocument () + +@property (weak) IBOutlet NSMenu *mapViewContextMenu; + +@end + +@implementation MapDocument { + NSPoint _mouseLocationForMapViewContextMenu; + NSUInteger _droppedPinCounter; + NSNumberFormatter *_spellOutNumberFormatter; + + BOOL _showsToolTipsOnDroppedPins; + BOOL _randomizesCursorsOnDroppedPins; + BOOL _isTouringWorld; +} + +#pragma mark Lifecycle + +- (NSString *)windowNibName { + return @"MapDocument"; +} + +- (void)windowControllerDidLoadNib:(NSWindowController *)aController { + [super windowControllerDidLoadNib:aController]; + + NSURL *savedURL = [[NSUserDefaults standardUserDefaults] URLForKey:@"MBXCurrentStyleURL"]; + if (savedURL) { + self.mapView.styleURL = savedURL; + } + + _spellOutNumberFormatter = [[NSNumberFormatter alloc] init]; + + NSPressGestureRecognizer *pressGestureRecognizer = [[NSPressGestureRecognizer alloc] initWithTarget:self action:@selector(handlePressGesture:)]; + [self.mapView addGestureRecognizer:pressGestureRecognizer]; +} + +- (NSData *)dataOfType:(NSString *)typeName error:(NSError **)outError { + // Insert code here to write your document to data of the specified type. If outError != NULL, ensure that you create and set an appropriate error when returning nil. + // You can also choose to override -fileWrapperOfType:error:, -writeToURL:ofType:error:, or -writeToURL:ofType:forSaveOperation:originalContentsURL:error: instead. + if (outError) { + *outError = [NSError errorWithDomain:NSOSStatusErrorDomain code:unimpErr userInfo:nil]; + } + return nil; +} + +- (BOOL)readFromData:(NSData *)data ofType:(NSString *)typeName error:(NSError **)outError { + // Insert code here to read your document from the given data of the specified type. If outError != NULL, ensure that you create and set an appropriate error when returning NO. + // You can also choose to override -readFromFileWrapper:ofType:error: or -readFromURL:ofType:error: instead. + // If you override either of these, you should also override -isEntireFileLoaded to return NO if the contents are lazily loaded. + if (outError) { + *outError = [NSError errorWithDomain:NSOSStatusErrorDomain code:unimpErr userInfo:nil]; + } + return NO; +} + ++ (BOOL)autosavesInPlace { + return YES; +} + +- (NSWindow *)window { + return self.windowControllers.firstObject.window; +} + +#pragma mark Services + +- (IBAction)showShareMenu:(id)sender { + NSSharingServicePicker *picker = [[NSSharingServicePicker alloc] initWithItems:@[self.shareURL]]; + picker.delegate = self; + [picker showRelativeToRect:[sender bounds] ofView:sender preferredEdge:NSMinYEdge]; +} + +- (NSURL *)shareURL { + NSArray *components = self.mapView.styleURL.pathComponents; + CLLocationCoordinate2D centerCoordinate = self.mapView.centerCoordinate; + return [NSURL URLWithString: + [NSString stringWithFormat:@"https://api.mapbox.com/styles/v1/%@/%@.html?access_token=%@#%.2f/%.5f/%.5f/%.f", + components[1], components[2], [MGLAccountManager accessToken], + self.mapView.zoomLevel, centerCoordinate.latitude, centerCoordinate.longitude, self.mapView.direction]]; +} + +#pragma mark View methods + +- (IBAction)setStyle:(id)sender { + NSInteger tag; + if ([sender isKindOfClass:[NSMenuItem class]]) { + tag = [sender tag]; + } else if ([sender isKindOfClass:[NSPopUpButton class]]) { + tag = [sender selectedTag]; + } + NSURL *styleURL; + switch (tag) { + case 1: + styleURL = [MGLStyle streetsStyleURL]; + break; + case 2: + styleURL = [MGLStyle emeraldStyleURL]; + break; + case 3: + styleURL = [MGLStyle lightStyleURL]; + break; + case 4: + styleURL = [MGLStyle darkStyleURL]; + break; + case 5: + styleURL = [MGLStyle satelliteStyleURL]; + break; + case 6: + styleURL = [MGLStyle hybridStyleURL]; + break; + default: + NSAssert(NO, @"Cannot set style from control with tag %li", (long)tag); + break; + } + self.mapView.styleURL = styleURL; + [[NSUserDefaults standardUserDefaults] setURL:self.mapView.styleURL forKey:@"MBXCurrentStyleURL"]; + [self.window.toolbar validateVisibleItems]; +} + +- (IBAction)chooseCustomStyle:(id)sender { + NSAlert *alert = [[NSAlert alloc] init]; + alert.messageText = @"Apply custom style"; + alert.informativeText = @"Enter the URL to a JSON file that conforms to the Mapbox GL style specification, such as a style designed in Mapbox Studio:"; + NSTextField *textField = [[NSTextField alloc] initWithFrame:NSZeroRect]; + [textField sizeToFit]; + NSRect textFieldFrame = textField.frame; + textFieldFrame.size.width = 300; + textField.frame = textFieldFrame; + NSURL *savedURL = [[NSUserDefaults standardUserDefaults] URLForKey:@"MBXCustomStyleURL"]; + if (savedURL) { + textField.stringValue = savedURL.absoluteString; + } + alert.accessoryView = textField; + [alert addButtonWithTitle:@"Apply"]; + [alert addButtonWithTitle:@"Cancel"]; + if ([alert runModal] == NSAlertFirstButtonReturn) { + self.mapView.styleURL = [NSURL URLWithString:textField.stringValue]; + [[NSUserDefaults standardUserDefaults] setURL:self.mapView.styleURL forKey:@"MBXCustomStyleURL"]; + [[NSUserDefaults standardUserDefaults] setURL:self.mapView.styleURL forKey:@"MBXCurrentStyleURL"]; + [self.window.toolbar validateVisibleItems]; + } +} + +- (IBAction)zoomIn:(id)sender { + [self.mapView setZoomLevel:self.mapView.zoomLevel + 1 animated:YES]; +} + +- (IBAction)zoomOut:(id)sender { + [self.mapView setZoomLevel:self.mapView.zoomLevel - 1 animated:YES]; +} + +- (IBAction)snapToNorth:(id)sender { + [self.mapView setDirection:0 animated:YES]; +} + +- (IBAction)reload:(id)sender { + [self.mapView reloadStyle:sender]; +} + +#pragma mark Debug methods + +- (IBAction)toggleTileBoundaries:(id)sender { + self.mapView.debugMask ^= MGLMapDebugTileBoundariesMask; +} + +- (IBAction)toggleTileInfo:(id)sender { + self.mapView.debugMask ^= MGLMapDebugTileInfoMask; +} + +- (IBAction)toggleTileTimestamps:(id)sender { + self.mapView.debugMask ^= MGLMapDebugTimestampsMask; +} + +- (IBAction)toggleCollisionBoxes:(id)sender { + self.mapView.debugMask ^= MGLMapDebugCollisionBoxesMask; +} + +- (IBAction)toggleShowsToolTipsOnDroppedPins:(id)sender { + _showsToolTipsOnDroppedPins = !_showsToolTipsOnDroppedPins; +} + +- (IBAction)toggleRandomizesCursorsOnDroppedPins:(id)sender { + _randomizesCursorsOnDroppedPins = !_randomizesCursorsOnDroppedPins; +} + +- (IBAction)dropManyPins:(id)sender { + [self.mapView removeAnnotations:self.mapView.annotations]; + + NSRect bounds = self.mapView.bounds; + NSMutableArray *annotations = [NSMutableArray array]; + for (CGFloat x = NSMinX(bounds); x < NSMaxX(bounds); x += arc4random_uniform(50)) { + for (CGFloat y = NSMaxY(bounds); y >= NSMinY(bounds); y -= arc4random_uniform(100)) { + [annotations addObject:[self pinAtPoint:NSMakePoint(x, y)]]; + } + } + + [NSTimer scheduledTimerWithTimeInterval:1/60 + target:self + selector:@selector(dropOneOfManyPins:) + userInfo:annotations + repeats:YES]; +} + +- (void)dropOneOfManyPins:(NSTimer *)timer { + NSMutableArray *annotations = timer.userInfo; + NSUInteger numberOfAnnotationsToAdd = 50; + if (annotations.count < numberOfAnnotationsToAdd) { + numberOfAnnotationsToAdd = annotations.count; + } + NSArray *annotationsToAdd = [annotations subarrayWithRange: + NSMakeRange(0, numberOfAnnotationsToAdd)]; + [self.mapView addAnnotations:annotationsToAdd]; + [annotations removeObjectsInRange:NSMakeRange(0, numberOfAnnotationsToAdd)]; + if (!annotations.count) { + [timer invalidate]; + } +} + +- (IBAction)removeAllPins:(id)sender { + [self.mapView removeAnnotations:self.mapView.annotations]; +} + +- (IBAction)startWorldTour:(id)sender { + _isTouringWorld = YES; + + [self removeAllPins:sender]; + NSUInteger numberOfAnnotations = sizeof(WorldTourDestinations) / sizeof(WorldTourDestinations[0]); + NSMutableArray *annotations = [NSMutableArray arrayWithCapacity:numberOfAnnotations]; + for (NSUInteger i = 0; i < numberOfAnnotations; i++) { + MGLPointAnnotation *annotation = [[MGLPointAnnotation alloc] init]; + annotation.coordinate = WorldTourDestinations[i]; + [annotations addObject:annotation]; + } + [self.mapView addAnnotations:annotations]; + [self continueWorldTourWithRemainingAnnotations:annotations]; +} + +- (void)continueWorldTourWithRemainingAnnotations:(NS_MUTABLE_ARRAY_OF(MGLPointAnnotation *) *)annotations { + MGLPointAnnotation *nextAnnotation = annotations.firstObject; + if (!nextAnnotation || !_isTouringWorld) { + _isTouringWorld = NO; + return; + } + + [annotations removeObjectAtIndex:0]; + MGLMapCamera *camera = [MGLMapCamera cameraLookingAtCenterCoordinate:nextAnnotation.coordinate + fromDistance:0 + pitch:arc4random_uniform(60) + heading:arc4random_uniform(360)]; + __weak MapDocument *weakSelf = self; + [self.mapView flyToCamera:camera completionHandler:^{ + MapDocument *strongSelf = weakSelf; + [strongSelf performSelector:@selector(continueWorldTourWithRemainingAnnotations:) + withObject:annotations + afterDelay:2]; + }]; +} + +- (IBAction)stopWorldTour:(id)sender { + _isTouringWorld = NO; + // Any programmatic viewpoint change cancels outstanding animations. + self.mapView.camera = self.mapView.camera; +} + +#pragma mark Help methods + +- (IBAction)giveFeedback:(id)sender { + CLLocationCoordinate2D centerCoordinate = self.mapView.centerCoordinate; + NSURL *feedbackURL = [NSURL URLWithString:[NSString stringWithFormat:@"https://www.mapbox.com/map-feedback/#/%.5f/%.5f/%.0f", + centerCoordinate.longitude, centerCoordinate.latitude, round(self.mapView.zoomLevel)]]; + [[NSWorkspace sharedWorkspace] openURL:feedbackURL]; +} + +#pragma mark Mouse events + +- (void)handlePressGesture:(NSPressGestureRecognizer *)gestureRecognizer { + if (gestureRecognizer.state == NSGestureRecognizerStateBegan) { + NSPoint location = [gestureRecognizer locationInView:self.mapView]; + [self dropPinAtPoint:location]; + } +} + +- (IBAction)dropPin:(NSMenuItem *)sender { + [self dropPinAtPoint:_mouseLocationForMapViewContextMenu]; +} + +- (void)dropPinAtPoint:(NSPoint)point { + DroppedPinAnnotation *annotation = [self pinAtPoint:point]; + [self.mapView addAnnotation:annotation]; + [self.mapView selectAnnotation:annotation]; +} + +- (DroppedPinAnnotation *)pinAtPoint:(NSPoint)point { + DroppedPinAnnotation *annotation = [[DroppedPinAnnotation alloc] init]; + annotation.coordinate = [self.mapView convertPoint:point toCoordinateFromView:self.mapView]; + annotation.title = @"Dropped Pin"; + _spellOutNumberFormatter.numberStyle = NSNumberFormatterSpellOutStyle; + if (_showsToolTipsOnDroppedPins) { + NSString *formattedNumber = [_spellOutNumberFormatter stringFromNumber:@(++_droppedPinCounter)]; + annotation.toolTip = formattedNumber; + } + return annotation; +} + +- (IBAction)removePin:(NSMenuItem *)sender { + [self removePinAtPoint:_mouseLocationForMapViewContextMenu]; +} + +- (void)removePinAtPoint:(NSPoint)point { + [self.mapView removeAnnotation:[self.mapView annotationAtPoint:point]]; +} + +#pragma mark User interface validation + +- (BOOL)validateMenuItem:(NSMenuItem *)menuItem { + if (menuItem.action == @selector(setStyle:)) { + NSURL *styleURL = self.mapView.styleURL; + NSCellStateValue state; + switch (menuItem.tag) { + case 1: + state = [styleURL isEqual:[MGLStyle streetsStyleURL]]; + break; + case 2: + state = [styleURL isEqual:[MGLStyle emeraldStyleURL]]; + break; + case 3: + state = [styleURL isEqual:[MGLStyle lightStyleURL]]; + break; + case 4: + state = [styleURL isEqual:[MGLStyle darkStyleURL]]; + break; + case 5: + state = [styleURL isEqual:[MGLStyle satelliteStyleURL]]; + break; + case 6: + state = [styleURL isEqual:[MGLStyle hybridStyleURL]]; + break; + default: + return NO; + } + menuItem.state = state; + return YES; + } + if (menuItem.action == @selector(chooseCustomStyle:)) { + menuItem.state = self.indexOfStyleInToolbarItem == NSNotFound; + return YES; + } + if (menuItem.action == @selector(zoomIn:)) { + return self.mapView.zoomLevel < self.mapView.maximumZoomLevel; + } + if (menuItem.action == @selector(zoomOut:)) { + return self.mapView.zoomLevel > self.mapView.minimumZoomLevel; + } + if (menuItem.action == @selector(snapToNorth:)) { + return self.mapView.direction != 0; + } + if (menuItem.action == @selector(reload:)) { + return YES; + } + if (menuItem.action == @selector(dropPin:)) { + BOOL isOverAnnotation = [self.mapView annotationAtPoint:_mouseLocationForMapViewContextMenu]; + menuItem.hidden = isOverAnnotation; + return YES; + } + if (menuItem.action == @selector(removePin:)) { + BOOL isOverAnnotation = [self.mapView annotationAtPoint:_mouseLocationForMapViewContextMenu]; + menuItem.hidden = !isOverAnnotation; + return YES; + } + if (menuItem.action == @selector(toggleTileBoundaries:)) { + BOOL isShown = self.mapView.debugMask & MGLMapDebugTileBoundariesMask; + menuItem.title = isShown ? @"Hide Tile Boundaries" : @"Show Tile Boundaries"; + return YES; + } + if (menuItem.action == @selector(toggleTileInfo:)) { + BOOL isShown = self.mapView.debugMask & MGLMapDebugTileInfoMask; + menuItem.title = isShown ? @"Hide Tile Info" : @"Show Tile Info"; + return YES; + } + if (menuItem.action == @selector(toggleTileTimestamps:)) { + BOOL isShown = self.mapView.debugMask & MGLMapDebugTimestampsMask; + menuItem.title = isShown ? @"Hide Tile Timestamps" : @"Show Tile Timestamps"; + return YES; + } + if (menuItem.action == @selector(toggleCollisionBoxes:)) { + BOOL isShown = self.mapView.debugMask & MGLMapDebugCollisionBoxesMask; + menuItem.title = isShown ? @"Hide Collision Boxes" : @"Show Collision Boxes"; + return YES; + } + if (menuItem.action == @selector(toggleShowsToolTipsOnDroppedPins:)) { + BOOL isShown = _showsToolTipsOnDroppedPins; + menuItem.title = isShown ? @"Hide Tooltips on Dropped Pins" : @"Show Tooltips on Dropped Pins"; + return YES; + } + if (menuItem.action == @selector(toggleRandomizesCursorsOnDroppedPins:)) { + BOOL isRandom = _randomizesCursorsOnDroppedPins; + menuItem.title = isRandom ? @"Use Default Cursor for Dropped Pins" : @"Use Random Cursors for Dropped Pins"; + return _showsToolTipsOnDroppedPins; + } + if (menuItem.action == @selector(dropManyPins:)) { + return YES; + } + if (menuItem.action == @selector(removeAllPins:)) { + return self.mapView.annotations.count; + } + if (menuItem.action == @selector(startWorldTour:)) { + return !_isTouringWorld; + } + if (menuItem.action == @selector(stopWorldTour:)) { + return _isTouringWorld; + } + if (menuItem.action == @selector(giveFeedback:)) { + return YES; + } + return NO; +} + +- (NSUInteger)indexOfStyleInToolbarItem { + if (![MGLAccountManager accessToken]) { + return NSNotFound; + } + + NSArray *styleURLs = @[ + [MGLStyle streetsStyleURL], + [MGLStyle emeraldStyleURL], + [MGLStyle lightStyleURL], + [MGLStyle darkStyleURL], + [MGLStyle satelliteStyleURL], + [MGLStyle hybridStyleURL], + ]; + return [styleURLs indexOfObject:self.mapView.styleURL]; +} + +- (BOOL)validateToolbarItem:(NSToolbarItem *)toolbarItem { + if (!self.mapView) { + return NO; + } + + if (toolbarItem.action == @selector(showShareMenu:)) { + [(NSButton *)toolbarItem.view sendActionOn:NSLeftMouseDownMask]; + if (![MGLAccountManager accessToken]) { + return NO; + } + NSURL *styleURL = self.mapView.styleURL; + return ([styleURL.scheme isEqualToString:@"mapbox"] + && [styleURL.pathComponents.firstObject isEqualToString:@"styles"]); + } + if (toolbarItem.action == @selector(setStyle:)) { + NSPopUpButton *popUpButton = (NSPopUpButton *)toolbarItem.view; + NSUInteger index = self.indexOfStyleInToolbarItem; + if (index == NSNotFound) { + [popUpButton addItemWithTitle:@"Custom"]; + index = [popUpButton numberOfItems] - 1; + } + [popUpButton selectItemAtIndex:index]; + } + return NO; +} + +#pragma mark NSSharingServicePickerDelegate methods + +- (NS_ARRAY_OF(NSSharingService *) *)sharingServicePicker:(NSSharingServicePicker *)sharingServicePicker sharingServicesForItems:(NSArray *)items proposedSharingServices:(NS_ARRAY_OF(NSSharingService *) *)proposedServices { + NSURL *shareURL = self.shareURL; + NSURL *browserURL = [[NSWorkspace sharedWorkspace] URLForApplicationToOpenURL:shareURL]; + NSImage *browserIcon = [[NSWorkspace sharedWorkspace] iconForFile:browserURL.path]; + NSString *browserName = [[NSFileManager defaultManager] displayNameAtPath:browserURL.path]; + NSString *browserServiceName = [NSString stringWithFormat:@"Open in %@", browserName]; + + NSSharingService *browserService = [[NSSharingService alloc] initWithTitle:browserServiceName + image:browserIcon + alternateImage:nil + handler:^{ + [[NSWorkspace sharedWorkspace] openURL:self.shareURL]; + }]; + + NSMutableArray *sharingServices = [proposedServices mutableCopy]; + [sharingServices insertObject:browserService atIndex:0]; + return sharingServices; +} + +#pragma mark NSMenuDelegate methods + +- (void)menuWillOpen:(NSMenu *)menu { + if (menu == self.mapViewContextMenu) { + _mouseLocationForMapViewContextMenu = [self.window.contentView convertPoint:self.window.mouseLocationOutsideOfEventStream + toView:self.mapView]; + } +} + +#pragma mark MGLMapViewDelegate methods + +- (BOOL)mapView:(MGLMapView *)mapView annotationCanShowCallout:(id )annotation { + return YES; +} + +- (MGLAnnotationImage *)mapView:(MGLMapView *)mapView imageForAnnotation:(id )annotation { + MGLAnnotationImage *annotationImage = [self.mapView dequeueReusableAnnotationImageWithIdentifier:MGLDroppedPinAnnotationImageIdentifier]; + if (!annotationImage) { + NSString *imagePath = [[NSBundle bundleForClass:[MGLMapView class]] + pathForResource:@"default_marker" ofType:@"pdf"]; + NSImage *image = [[NSImage alloc] initWithContentsOfFile:imagePath]; + NSRect alignmentRect = image.alignmentRect; + alignmentRect.origin.y = NSMidY(alignmentRect); + alignmentRect.size.height /= 2; + image.alignmentRect = alignmentRect; + annotationImage = [MGLAnnotationImage annotationImageWithImage:image + reuseIdentifier:MGLDroppedPinAnnotationImageIdentifier]; + } + if (_randomizesCursorsOnDroppedPins) { + NSArray *cursors = @[ + [NSCursor IBeamCursor], + [NSCursor crosshairCursor], + [NSCursor pointingHandCursor], + [NSCursor disappearingItemCursor], + [NSCursor IBeamCursorForVerticalLayout], + [NSCursor operationNotAllowedCursor], + [NSCursor dragLinkCursor], + [NSCursor dragCopyCursor], + [NSCursor contextualMenuCursor], + ]; + annotationImage.cursor = cursors[arc4random_uniform(cursors.count)]; + } else { + annotationImage.cursor = nil; + } + return annotationImage; +} + +- (void)mapView:(MGLMapView *)mapView didSelectAnnotation:(id )annotation { + if ([annotation isKindOfClass:[DroppedPinAnnotation class]]) { + DroppedPinAnnotation *droppedPin = annotation; + [droppedPin resume]; + } +} + +- (void)mapView:(MGLMapView *)mapView didDeselectAnnotation:(id )annotation { + if ([annotation isKindOfClass:[DroppedPinAnnotation class]]) { + DroppedPinAnnotation *droppedPin = annotation; + [droppedPin pause]; + } +} + +@end + +@interface ValidatedToolbarItem : NSToolbarItem + +@end + +@implementation ValidatedToolbarItem + +- (void)validate { + [(MapDocument *)self.toolbar.delegate validateToolbarItem:self]; +} + +@end diff --git a/platform/osx/app/MapDocument.xib b/platform/osx/app/MapDocument.xib new file mode 100644 index 000000000..3adbba8c1 --- /dev/null +++ b/platform/osx/app/MapDocument.xib @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + %{title1}@ + LocationCoordinate2DTransformer + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/platform/osx/app/mapboxgl-app.gypi b/platform/osx/app/mapboxgl-app.gypi index ca096143c..7b39f5d9e 100644 --- a/platform/osx/app/mapboxgl-app.gypi +++ b/platform/osx/app/mapboxgl-app.gypi @@ -13,6 +13,7 @@ 'Credits.rtf', 'Icon.icns', 'MainMenu.xib', + 'MapDocument.xib', ], 'dependencies': [ @@ -26,6 +27,8 @@ './DroppedPinAnnotation.m', './LocationCoordinate2DTransformer.h', './LocationCoordinate2DTransformer.m', + './MapDocument.h', + './MapDocument.m', './TimeIntervalTransformer.h', './TimeIntervalTransformer.m', './NSValue+Additions.h',