Added documentation comments for all the public headers that aren’t being shared with iOS. Removed an animated: parameter from -selectAnnotation: and -deselectAnnotation:: because callout popovers may extend beyond the entire window, there is no need to scroll the map to make the entire callout visible. Added missing geometric conversion methods. Renamed -mapView:regionWillChangeAnimated: et al. to say “camera” instead of “region”. “Region” leaves ambiguity about whether properties like rotation and pitch trigger this method. “Camera” associates these methods with the camera property, which seems apt.
585 lines
22 KiB
Objective-C
585 lines
22 KiB
Objective-C
#import "AppDelegate.h"
|
|
|
|
#import "DroppedPinAnnotation.h"
|
|
#import "LocationCoordinate2DTransformer.h"
|
|
#import "NSValue+Additions.h"
|
|
|
|
#import <Mapbox/Mapbox.h>
|
|
|
|
static NSString * const MGLMapboxAccessTokenDefaultsKey = @"MGLMapboxAccessToken";
|
|
static NSString * const MGLDroppedPinAnnotationImageIdentifier = @"dropped";
|
|
|
|
@interface AppDelegate () <NSApplicationDelegate, NSSharingServicePickerDelegate, NSMenuDelegate, MGLMapViewDelegate>
|
|
|
|
@property (weak) IBOutlet NSWindow *window;
|
|
@property (weak) IBOutlet MGLMapView *mapView;
|
|
@property (weak) IBOutlet NSMenu *mapViewContextMenu;
|
|
|
|
@property (weak) IBOutlet NSWindow *preferencesWindow;
|
|
|
|
@end
|
|
|
|
@implementation AppDelegate {
|
|
NSPoint _mouseLocationForMapViewContextMenu;
|
|
NSUInteger _droppedPinCounter;
|
|
NSNumberFormatter *_spellOutNumberFormatter;
|
|
|
|
BOOL _showsToolTipsOnDroppedPins;
|
|
BOOL _randomizesCursorsOnDroppedPins;
|
|
}
|
|
|
|
#pragma mark Lifecycle
|
|
|
|
+ (void)load {
|
|
// Set access token, unless MGLAccountManager already read it in from Info.plist.
|
|
if (![MGLAccountManager accessToken]) {
|
|
NSString *accessToken = [NSProcessInfo processInfo].environment[@"MAPBOX_ACCESS_TOKEN"];
|
|
if (accessToken) {
|
|
// Store to preferences so that we can launch the app later on without having to specify
|
|
// token.
|
|
[[NSUserDefaults standardUserDefaults] setObject:accessToken forKey:MGLMapboxAccessTokenDefaultsKey];
|
|
} else {
|
|
// Try to retrieve from preferences, maybe we've stored them there previously and can reuse
|
|
// the token.
|
|
accessToken = [[NSUserDefaults standardUserDefaults] stringForKey:MGLMapboxAccessTokenDefaultsKey];
|
|
}
|
|
[MGLAccountManager setAccessToken:accessToken];
|
|
}
|
|
}
|
|
|
|
- (void)applicationWillFinishLaunching:(NSNotification *)notification {
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(userDefaultsDidChange:)
|
|
name:NSUserDefaultsDidChangeNotification
|
|
object:nil];
|
|
|
|
[[NSAppleEventManager sharedAppleEventManager] setEventHandler:self
|
|
andSelector:@selector(handleGetURLEvent:withReplyEvent:)
|
|
forEventClass:kInternetEventClass
|
|
andEventID:kAEGetURL];
|
|
}
|
|
|
|
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
|
|
// Set access token, unless MGLAccountManager already read it in from Info.plist.
|
|
if (![MGLAccountManager accessToken]) {
|
|
NSAlert *alert = [[NSAlert alloc] init];
|
|
alert.messageText = @"Access token required";
|
|
alert.informativeText = @"To load Mapbox-hosted tiles and styles, enter your Mapbox access token in Preferences.";
|
|
[alert addButtonWithTitle:@"Open Preferences"];
|
|
[alert runModal];
|
|
[self showPreferences:nil];
|
|
}
|
|
|
|
_spellOutNumberFormatter = [[NSNumberFormatter alloc] init];
|
|
|
|
NSPressGestureRecognizer *pressGestureRecognizer = [[NSPressGestureRecognizer alloc] initWithTarget:self action:@selector(handlePressGesture:)];
|
|
[self.mapView addGestureRecognizer:pressGestureRecognizer];
|
|
}
|
|
|
|
- (void)applicationWillTerminate:(NSNotification *)notification {
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
|
}
|
|
|
|
- (void)userDefaultsDidChange:(NSNotification *)notification {
|
|
NSUserDefaults *userDefaults = notification.object;
|
|
NSString *accessToken = [userDefaults stringForKey:MGLMapboxAccessTokenDefaultsKey];
|
|
if (![accessToken isEqualToString:[MGLAccountManager accessToken]]) {
|
|
[MGLAccountManager setAccessToken:accessToken];
|
|
[self reload:self];
|
|
}
|
|
}
|
|
|
|
#pragma mark Services
|
|
|
|
- (void)handleGetURLEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent {
|
|
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:@"&"]) {
|
|
NSArray *parts = [param componentsSeparatedByString:@"="];
|
|
if (parts.count >= 2) {
|
|
params[parts[0]] = [parts[1] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
|
|
}
|
|
}
|
|
|
|
NSString *centerString = params[@"center"];
|
|
if (centerString) {
|
|
NSArray *coordinateValues = [centerString componentsSeparatedByString:@","];
|
|
if (coordinateValues.count == 2) {
|
|
self.mapView.centerCoordinate = CLLocationCoordinate2DMake([coordinateValues[0] doubleValue],
|
|
[coordinateValues[1] doubleValue]);
|
|
}
|
|
}
|
|
|
|
NSString *zoomLevelString = params[@"zoom"];
|
|
if (zoomLevelString.length) {
|
|
self.mapView.zoomLevel = zoomLevelString.doubleValue;
|
|
}
|
|
|
|
NSString *directionString = params[@"bearing"];
|
|
if (directionString.length) {
|
|
self.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;
|
|
[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:";
|
|
NSTextField *textField = [[NSTextField alloc] initWithFrame:NSZeroRect];
|
|
[textField sizeToFit];
|
|
NSRect textFieldFrame = textField.frame;
|
|
textFieldFrame.size.width = 300;
|
|
textField.frame = textFieldFrame;
|
|
NSString *savedURLString = [[NSUserDefaults standardUserDefaults] stringForKey:@"MBXCustomStyleURL"];
|
|
if (savedURLString) {
|
|
textField.stringValue = savedURLString;
|
|
}
|
|
alert.accessoryView = textField;
|
|
[alert addButtonWithTitle:@"Apply"];
|
|
[alert addButtonWithTitle:@"Cancel"];
|
|
if ([alert runModal] == NSAlertFirstButtonReturn) {
|
|
[[NSUserDefaults standardUserDefaults] setObject:textField.stringValue forKey:@"MBXCustomStyleURL"];
|
|
self.mapView.styleURL = [NSURL URLWithString:textField.stringValue];
|
|
[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];
|
|
}
|
|
}
|
|
|
|
#pragma mark Help methods
|
|
|
|
- (IBAction)showShortcuts:(id)sender {
|
|
NSAlert *alert = [[NSAlert alloc] init];
|
|
alert.messageText = @"Mapbox GL Help";
|
|
alert.informativeText = @"\
|
|
• To scroll, swipe with two fingers, drag the cursor, or press the arrow keys.\n\
|
|
• To zoom, pinch with two fingers, or hold down Shift while dragging the cursor up and down, or hold down Option while pressing the up and down arrow keys.\n\
|
|
• To rotate, move two fingers opposite each other in a circle, or hold down Option while dragging the cursor left and right, or hold down Option while pressing the left and right arrow keys.\n\
|
|
• To tilt, hold down Option while dragging the cursor up and down.\
|
|
• To drop a pin, click and hold.\
|
|
";
|
|
[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];
|
|
}
|
|
|
|
- (IBAction)openAccessTokenManager:(id)sender {
|
|
[[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(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];
|
|
return ([MGLAccountManager accessToken]
|
|
&& [self.mapView.styleURL.scheme isEqualToString:@"mapbox"]
|
|
&& [self.mapView.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 <MGLAnnotation>)annotation {
|
|
return YES;
|
|
}
|
|
|
|
- (MGLAnnotationImage *)mapView:(MGLMapView *)mapView imageForAnnotation:(id <MGLAnnotation>)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 <MGLAnnotation>)annotation {
|
|
if ([annotation isKindOfClass:[DroppedPinAnnotation class]]) {
|
|
DroppedPinAnnotation *droppedPin = annotation;
|
|
[droppedPin resume];
|
|
}
|
|
}
|
|
|
|
- (void)mapView:(MGLMapView *)mapView didDeselectAnnotation:(id <MGLAnnotation>)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
|