diff options
Diffstat (limited to 'platform/macos/app')
29 files changed, 2487 insertions, 0 deletions
diff --git a/platform/macos/app/AppDelegate.h b/platform/macos/app/AppDelegate.h new file mode 100644 index 0000000000..a1d9297b2f --- /dev/null +++ b/platform/macos/app/AppDelegate.h @@ -0,0 +1,24 @@ +#import <Mapbox/Mapbox.h> + +extern NSString * const MGLMapboxAccessTokenDefaultsKey; + +@interface AppDelegate : NSObject <NSApplicationDelegate> + +@property (weak) IBOutlet NSWindow *preferencesWindow; + +// Normally, an application should respect the “Close windows when quitting an +// application” setting in the General pane of System Preferences. But the map +// would only be restored to its last opened location if the user quits the +// application using Quit and Keep Windows. An application that displays only a +// map should restore the last viewed map, like Maps.app does. These properties +// temporarily hold state for the next map window to be opened. + +@property (assign) double pendingZoomLevel; +@property (copy) MGLMapCamera *pendingCamera; +@property (assign) MGLCoordinateBounds pendingVisibleCoordinateBounds; +@property (assign) double pendingMinimumZoomLevel; +@property (assign) double pendingMaximumZoomLevel; +@property (copy) NSURL *pendingStyleURL; +@property (assign) MGLMapDebugMaskOptions pendingDebugMask; + +@end diff --git a/platform/macos/app/AppDelegate.m b/platform/macos/app/AppDelegate.m new file mode 100644 index 0000000000..ce5abcae20 --- /dev/null +++ b/platform/macos/app/AppDelegate.m @@ -0,0 +1,286 @@ +#import "AppDelegate.h" + +#import "MapDocument.h" + +NSString * const MGLMapboxAccessTokenDefaultsKey = @"MGLMapboxAccessToken"; +NSString * const MGLLastMapCameraDefaultsKey = @"MGLLastMapCamera"; +NSString * const MGLLastMapStyleURLDefaultsKey = @"MGLLastMapStyleURL"; +NSString * const MGLLastMapDebugMaskDefaultsKey = @"MGLLastMapDebugMask"; + +/** + Some convenience methods to make offline pack properties easier to bind to. + */ +@implementation MGLOfflinePack (Additions) + ++ (NSSet *)keyPathsForValuesAffectingStateImage { + return [NSSet setWithObjects:@"state", nil]; +} + +- (NSImage *)stateImage { + switch (self.state) { + case MGLOfflinePackStateComplete: + return [NSImage imageNamed:@"NSMenuOnStateTemplate"]; + + case MGLOfflinePackStateActive: + return [NSImage imageNamed:@"NSFollowLinkFreestandingTemplate"]; + + default: + return nil; + } +} + ++ (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingCountOfResourcesCompleted { + return [NSSet setWithObjects:@"progress", nil]; +} + +- (uint64_t)countOfResourcesCompleted { + return self.progress.countOfResourcesCompleted; +} + ++ (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingCountOfResourcesExpected { + return [NSSet setWithObjects:@"progress", nil]; +} + +- (uint64_t)countOfResourcesExpected { + return self.progress.countOfResourcesExpected; +} + ++ (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingCountOfBytesCompleted { + return [NSSet setWithObjects:@"progress", nil]; +} + +- (uint64_t)countOfBytesCompleted { + return self.progress.countOfBytesCompleted; +} + ++ (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingCountOfTilesCompleted { + return [NSSet setWithObjects:@"progress", nil]; +} + +- (uint64_t)countOfTilesCompleted { + return self.progress.countOfTilesCompleted; +} + ++ (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingCountOfTileBytesCompleted { + return [NSSet setWithObjects:@"progress", nil]; +} + +- (uint64_t)countOfTileBytesCompleted { + return self.progress.countOfTileBytesCompleted; +} + +@end + +@interface AppDelegate () + +@property (weak) IBOutlet NSArrayController *offlinePacksArrayController; +@property (weak) IBOutlet NSPanel *offlinePacksPanel; + +@end + +@implementation AppDelegate + +#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 { + [[NSAppleEventManager sharedAppleEventManager] setEventHandler:self + andSelector:@selector(handleGetURLEvent:withReplyEvent:) + forEventClass:kInternetEventClass + andEventID:kAEGetURL]; + + if (![[NSUserDefaults standardUserDefaults] boolForKey:@"NSQuitAlwaysKeepsWindows"]) { + NSData *cameraData = [[NSUserDefaults standardUserDefaults] objectForKey:MGLLastMapCameraDefaultsKey]; + if (cameraData) { + NSKeyedUnarchiver *coder = [[NSKeyedUnarchiver alloc] initForReadingWithData:cameraData]; + self.pendingZoomLevel = -1; + self.pendingCamera = [[MGLMapCamera alloc] initWithCoder:coder]; + } + NSString *styleURLString = [[NSUserDefaults standardUserDefaults] objectForKey:MGLLastMapStyleURLDefaultsKey]; + if (styleURLString) { + self.pendingStyleURL = [NSURL URLWithString:styleURLString]; + } + self.pendingDebugMask = [[NSUserDefaults standardUserDefaults] integerForKey:MGLLastMapDebugMaskDefaultsKey]; + } +} + +- (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]; + } + + [self.offlinePacksArrayController bind:@"content" toObject:[MGLOfflineStorage sharedOfflineStorage] withKeyPath:@"packs" options:nil]; +} + +- (void)applicationWillTerminate:(NSNotification *)notification { + if (![[NSUserDefaults standardUserDefaults] boolForKey:@"NSQuitAlwaysKeepsWindows"]) { + NSDocument *currentDocument = [NSDocumentController sharedDocumentController].currentDocument; + if ([currentDocument isKindOfClass:[MapDocument class]]) { + MGLMapView *mapView = [(MapDocument *)currentDocument mapView]; + NSMutableData *cameraData = [NSMutableData data]; + NSKeyedArchiver *coder = [[NSKeyedArchiver alloc] initForWritingWithMutableData:cameraData]; + [mapView.camera encodeWithCoder:coder]; + [coder finishEncoding]; + [[NSUserDefaults standardUserDefaults] setObject:cameraData forKey:MGLLastMapCameraDefaultsKey]; + [[NSUserDefaults standardUserDefaults] setObject:mapView.styleURL.absoluteString forKey:MGLLastMapStyleURLDefaultsKey]; + [[NSUserDefaults standardUserDefaults] setInteger:mapView.debugMask forKey:MGLLastMapDebugMaskDefaultsKey]; + } + } + + [self.offlinePacksArrayController unbind:@"content"]; +} + +#pragma mark Services + +- (void)handleGetURLEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent { + // mapboxgl://?center=29.95,-90.066667&zoom=14&bearing=45&pitch=30 + 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]; + } + } + + MGLMapCamera *camera = [MGLMapCamera camera]; + NSString *zoomLevelString = params[@"zoom"]; + self.pendingZoomLevel = zoomLevelString.length ? zoomLevelString.doubleValue : -1; + + NSString *directionString = params[@"bearing"]; + if (directionString.length) { + camera.heading = directionString.doubleValue; + } + + NSString *centerString = params[@"center"]; + if (centerString) { + NSArray *coordinateValues = [centerString componentsSeparatedByString:@","]; + if (coordinateValues.count == 2) { + camera.centerCoordinate = CLLocationCoordinate2DMake([coordinateValues[0] doubleValue], + [coordinateValues[1] doubleValue]); + } + } + + NSString *pitchString = params[@"pitch"]; + if (pitchString.length) { + camera.pitch = pitchString.doubleValue; + } + + self.pendingCamera = camera; + [[NSDocumentController sharedDocumentController] openUntitledDocumentAndDisplay:YES error:NULL]; +} + +#pragma mark Offline pack management + +- (IBAction)showOfflinePacksPanel:(id)sender { + [self.offlinePacksPanel makeKeyAndOrderFront:sender]; + + for (MGLOfflinePack *pack in self.offlinePacksArrayController.arrangedObjects) { + [pack requestProgress]; + } +} + +- (IBAction)delete:(id)sender { + for (MGLOfflinePack *pack in self.offlinePacksArrayController.selectedObjects) { + [[MGLOfflineStorage sharedOfflineStorage] removePack:pack withCompletionHandler:^(NSError * _Nullable error) { + if (error) { + [[NSAlert alertWithError:error] runModal]; + } + }]; + } +} + +- (IBAction)chooseOfflinePack:(id)sender { + for (MGLOfflinePack *pack in self.offlinePacksArrayController.selectedObjects) { + switch (pack.state) { + case MGLOfflinePackStateComplete: + { + if ([pack.region isKindOfClass:[MGLTilePyramidOfflineRegion class]]) { + MGLTilePyramidOfflineRegion *region = (MGLTilePyramidOfflineRegion *)pack.region; + self.pendingVisibleCoordinateBounds = region.bounds; + self.pendingMinimumZoomLevel = region.minimumZoomLevel; + self.pendingMaximumZoomLevel = region.maximumZoomLevel; + [[NSDocumentController sharedDocumentController] openUntitledDocumentAndDisplay:YES error:NULL]; + } + break; + } + + case MGLOfflinePackStateInactive: + [pack resume]; + break; + + case MGLOfflinePackStateActive: + [pack suspend]; + break; + + default: + break; + } + } +} + +#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 on a trackpad, or drag the cursor, or press the arrow keys.\n\ +• To zoom in, pinch two fingers apart on a trackpad, or double-click, or hold down Shift while dragging the cursor down, or hold down Option while pressing the up key.\n\ +• To zoom out, pinch two fingers together on a trackpad, or double-tap on a mouse, or hold down Shift while dragging the cursor up, or hold down Option while pressing the down key.\n\ +• To rotate, move two fingers opposite each other in a circle on a trackpad, 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.\n\ +• To drop a pin, click and hold.\ +"; + [alert runModal]; +} + +- (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 User interface validation + +- (BOOL)validateMenuItem:(NSMenuItem *)menuItem { + if (menuItem.action == @selector(showShortcuts:)) { + return YES; + } + if (menuItem.action == @selector(showPreferences:)) { + return YES; + } + if (menuItem.action == @selector(showOfflinePacksPanel:)) { + return YES; + } + if (menuItem.action == @selector(delete:)) { + return self.offlinePacksArrayController.selectedObjects.count; + } + return NO; +} + +@end diff --git a/platform/macos/app/Assets.xcassets/AppIcon.appiconset/AppIcon128x128.png b/platform/macos/app/Assets.xcassets/AppIcon.appiconset/AppIcon128x128.png Binary files differnew file mode 100644 index 0000000000..145d5a7d85 --- /dev/null +++ b/platform/macos/app/Assets.xcassets/AppIcon.appiconset/AppIcon128x128.png diff --git a/platform/macos/app/Assets.xcassets/AppIcon.appiconset/AppIcon16x16.png b/platform/macos/app/Assets.xcassets/AppIcon.appiconset/AppIcon16x16.png Binary files differnew file mode 100644 index 0000000000..fa2588dec3 --- /dev/null +++ b/platform/macos/app/Assets.xcassets/AppIcon.appiconset/AppIcon16x16.png diff --git a/platform/macos/app/Assets.xcassets/AppIcon.appiconset/AppIcon256x256-1.png b/platform/macos/app/Assets.xcassets/AppIcon.appiconset/AppIcon256x256-1.png Binary files differnew file mode 100644 index 0000000000..18fec77f84 --- /dev/null +++ b/platform/macos/app/Assets.xcassets/AppIcon.appiconset/AppIcon256x256-1.png diff --git a/platform/macos/app/Assets.xcassets/AppIcon.appiconset/AppIcon256x256.png b/platform/macos/app/Assets.xcassets/AppIcon.appiconset/AppIcon256x256.png Binary files differnew file mode 100644 index 0000000000..18fec77f84 --- /dev/null +++ b/platform/macos/app/Assets.xcassets/AppIcon.appiconset/AppIcon256x256.png diff --git a/platform/macos/app/Assets.xcassets/AppIcon.appiconset/AppIcon32x32-1.png b/platform/macos/app/Assets.xcassets/AppIcon.appiconset/AppIcon32x32-1.png Binary files differnew file mode 100644 index 0000000000..bf3acc1282 --- /dev/null +++ b/platform/macos/app/Assets.xcassets/AppIcon.appiconset/AppIcon32x32-1.png diff --git a/platform/macos/app/Assets.xcassets/AppIcon.appiconset/AppIcon32x32.png b/platform/macos/app/Assets.xcassets/AppIcon.appiconset/AppIcon32x32.png Binary files differnew file mode 100644 index 0000000000..bf3acc1282 --- /dev/null +++ b/platform/macos/app/Assets.xcassets/AppIcon.appiconset/AppIcon32x32.png diff --git a/platform/macos/app/Assets.xcassets/AppIcon.appiconset/AppIcon512x512-1.png b/platform/macos/app/Assets.xcassets/AppIcon.appiconset/AppIcon512x512-1.png Binary files differnew file mode 100644 index 0000000000..1ea7683696 --- /dev/null +++ b/platform/macos/app/Assets.xcassets/AppIcon.appiconset/AppIcon512x512-1.png diff --git a/platform/macos/app/Assets.xcassets/AppIcon.appiconset/AppIcon512x512.png b/platform/macos/app/Assets.xcassets/AppIcon.appiconset/AppIcon512x512.png Binary files differnew file mode 100644 index 0000000000..1ea7683696 --- /dev/null +++ b/platform/macos/app/Assets.xcassets/AppIcon.appiconset/AppIcon512x512.png diff --git a/platform/macos/app/Assets.xcassets/AppIcon.appiconset/Contents.json b/platform/macos/app/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..58e739d056 --- /dev/null +++ b/platform/macos/app/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "AppIcon16x16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "AppIcon32x32-1.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "AppIcon32x32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "icon-1.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "AppIcon128x128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "AppIcon256x256-1.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "AppIcon256x256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "AppIcon512x512-1.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "AppIcon512x512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "icon.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +}
\ No newline at end of file diff --git a/platform/macos/app/Assets.xcassets/AppIcon.appiconset/icon-1.png b/platform/macos/app/Assets.xcassets/AppIcon.appiconset/icon-1.png Binary files differnew file mode 100644 index 0000000000..36dd7acf90 --- /dev/null +++ b/platform/macos/app/Assets.xcassets/AppIcon.appiconset/icon-1.png diff --git a/platform/macos/app/Assets.xcassets/AppIcon.appiconset/icon.png b/platform/macos/app/Assets.xcassets/AppIcon.appiconset/icon.png Binary files differnew file mode 100644 index 0000000000..fdee900aa4 --- /dev/null +++ b/platform/macos/app/Assets.xcassets/AppIcon.appiconset/icon.png diff --git a/platform/macos/app/Assets.xcassets/Contents.json b/platform/macos/app/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..da4a164c91 --- /dev/null +++ b/platform/macos/app/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +}
\ No newline at end of file diff --git a/platform/macos/app/Base.lproj/MainMenu.xib b/platform/macos/app/Base.lproj/MainMenu.xib new file mode 100644 index 0000000000..72a97aa8cf --- /dev/null +++ b/platform/macos/app/Base.lproj/MainMenu.xib @@ -0,0 +1,889 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct"> + <dependencies> + <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="10117"/> + </dependencies> + <objects> + <customObject id="-2" userLabel="File's Owner" customClass="NSApplication"> + <connections> + <outlet property="delegate" destination="Voe-Tx-rLC" id="GzC-gU-4Uq"/> + </connections> + </customObject> + <customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/> + <customObject id="-3" userLabel="Application" customClass="NSObject"> + <connections> + <outlet property="delegate" destination="Voe-Tx-rLC" id="z9N-Fm-MUP"/> + </connections> + </customObject> + <customObject id="Voe-Tx-rLC" customClass="AppDelegate"> + <connections> + <outlet property="offlinePacksArrayController" destination="dWe-R6-sRz" id="Ar5-xu-ABm"/> + <outlet property="offlinePacksPanel" destination="Jjv-gs-Tx6" id="0vK-rR-3ZX"/> + <outlet property="preferencesWindow" destination="UWc-yQ-qda" id="Ota-aT-Mz2"/> + </connections> + </customObject> + <menu title="Main Menu" systemMenu="main" id="AYu-sK-qS6"> + <items> + <menuItem title="Mapbox GL" id="1Xt-HY-uBw"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Mapbox GL" systemMenu="apple" id="uQy-DD-JDr"> + <items> + <menuItem title="About Mapbox GL" id="5kV-Vb-QxS"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="orderFrontStandardAboutPanel:" target="-1" id="Exp-CZ-Vem"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/> + <menuItem title="Preferences…" keyEquivalent="," id="BOF-NM-1cW"> + <connections> + <action selector="showPreferences:" target="-1" id="Llx-Uy-HTS"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="wFC-TO-SCJ"/> + <menuItem title="Services" id="NMo-om-nkz"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Services" systemMenu="services" id="hz9-B4-Xy5"/> + </menuItem> + <menuItem isSeparatorItem="YES" id="4je-JR-u6R"/> + <menuItem title="Hide Mapbox GL" keyEquivalent="h" id="Olw-nP-bQN"> + <connections> + <action selector="hide:" target="-1" id="PnN-Uc-m68"/> + </connections> + </menuItem> + <menuItem title="Hide Others" keyEquivalent="h" id="Vdr-fp-XzO"> + <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/> + <connections> + <action selector="hideOtherApplications:" target="-1" id="VT4-aY-XCT"/> + </connections> + </menuItem> + <menuItem title="Show All" id="Kd2-mp-pUS"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="unhideAllApplications:" target="-1" id="Dhg-Le-xox"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="kCx-OE-vgT"/> + <menuItem title="Quit Mapbox GL" keyEquivalent="q" id="4sb-4s-VLi"> + <connections> + <action selector="terminate:" target="-1" id="Te7-pn-YzF"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="File" id="dMs-cI-mzQ"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="File" id="bib-Uj-vzu"> + <items> + <menuItem title="New" keyEquivalent="n" id="Was-JA-tGl"> + <connections> + <action selector="newDocument:" target="-1" id="4Si-XN-c54"/> + </connections> + </menuItem> + <menuItem title="Open…" keyEquivalent="o" id="IAo-SY-fd9"> + <connections> + <action selector="openDocument:" target="-1" id="bVn-NM-KNZ"/> + </connections> + </menuItem> + <menuItem title="Open Recent" id="tXI-mr-wws"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Open Recent" systemMenu="recentDocuments" id="oas-Oc-fiZ"> + <items> + <menuItem title="Clear Menu" id="vNY-rz-j42"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="clearRecentDocuments:" target="-1" id="Daa-9d-B3U"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem isSeparatorItem="YES" id="m54-Is-iLE"/> + <menuItem title="Close" keyEquivalent="w" id="DVo-aG-piG"> + <connections> + <action selector="performClose:" target="-1" id="HmO-Ls-i7Q"/> + </connections> + </menuItem> + <menuItem title="Save…" keyEquivalent="s" id="pxx-59-PXV"> + <connections> + <action selector="saveDocument:" target="-1" id="teZ-XB-qJY"/> + </connections> + </menuItem> + <menuItem title="Save As…" keyEquivalent="S" id="Bw7-FT-i3A"> + <connections> + <action selector="saveDocumentAs:" target="-1" id="mDf-zr-I0C"/> + </connections> + </menuItem> + <menuItem title="Save Offline Pack…" id="UXB-sj-C7i"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="addOfflinePack:" target="-1" id="Usu-xO-QEx"/> + </connections> + </menuItem> + <menuItem title="Revert to Saved" id="KaW-ft-85H"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="revertDocumentToSaved:" target="-1" id="iJ3-Pv-kwq"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="aJh-i4-bef"/> + <menuItem title="Page Setup…" keyEquivalent="P" id="qIS-W8-SiK"> + <modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/> + <connections> + <action selector="runPageLayout:" target="-1" id="Din-rz-gC5"/> + </connections> + </menuItem> + <menuItem title="Print…" keyEquivalent="p" id="aTl-1u-JFS"> + <connections> + <action selector="print:" target="-1" id="qaZ-4w-aoO"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="Edit" id="5QF-Oa-p0T"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Edit" id="W48-6f-4Dl"> + <items> + <menuItem title="Undo" keyEquivalent="z" id="dRJ-4n-Yzg"> + <connections> + <action selector="undo:" target="-1" id="M6e-cu-g7V"/> + </connections> + </menuItem> + <menuItem title="Redo" keyEquivalent="Z" id="6dh-zS-Vam"> + <connections> + <action selector="redo:" target="-1" id="oIA-Rs-6OD"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="WRV-NI-Exz"/> + <menuItem title="Cut" keyEquivalent="x" id="uRl-iY-unG"> + <connections> + <action selector="cut:" target="-1" id="YJe-68-I9s"/> + </connections> + </menuItem> + <menuItem title="Copy" keyEquivalent="c" id="x3v-GG-iWU"> + <connections> + <action selector="copy:" target="-1" id="G1f-GL-Joy"/> + </connections> + </menuItem> + <menuItem title="Paste" keyEquivalent="v" id="gVA-U4-sdL"> + <connections> + <action selector="paste:" target="-1" id="UvS-8e-Qdg"/> + </connections> + </menuItem> + <menuItem title="Paste and Match Style" keyEquivalent="V" id="WeT-3V-zwk"> + <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/> + <connections> + <action selector="pasteAsPlainText:" target="-1" id="cEh-KX-wJQ"/> + </connections> + </menuItem> + <menuItem title="Delete" id="pa3-QI-u2k"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="delete:" target="-1" id="0Mk-Ml-PaM"/> + </connections> + </menuItem> + <menuItem title="Select All" keyEquivalent="a" id="Ruw-6m-B2m"> + <connections> + <action selector="selectAll:" target="-1" id="VNm-Mi-diN"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="uyl-h8-XO2"/> + <menuItem title="Find" id="4EN-yA-p0u"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Find" id="1b7-l0-nxx"> + <items> + <menuItem title="Find…" tag="1" keyEquivalent="f" id="Xz5-n4-O0W"> + <connections> + <action selector="performFindPanelAction:" target="-1" id="cD7-Qs-BN4"/> + </connections> + </menuItem> + <menuItem title="Find and Replace…" tag="12" keyEquivalent="f" id="YEy-JH-Tfz"> + <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/> + <connections> + <action selector="performFindPanelAction:" target="-1" id="WD3-Gg-5AJ"/> + </connections> + </menuItem> + <menuItem title="Find Next" tag="2" keyEquivalent="g" id="q09-fT-Sye"> + <connections> + <action selector="performFindPanelAction:" target="-1" id="NDo-RZ-v9R"/> + </connections> + </menuItem> + <menuItem title="Find Previous" tag="3" keyEquivalent="G" id="OwM-mh-QMV"> + <connections> + <action selector="performFindPanelAction:" target="-1" id="HOh-sY-3ay"/> + </connections> + </menuItem> + <menuItem title="Use Selection for Find" tag="7" keyEquivalent="e" id="buJ-ug-pKt"> + <connections> + <action selector="performFindPanelAction:" target="-1" id="U76-nv-p5D"/> + </connections> + </menuItem> + <menuItem title="Jump to Selection" keyEquivalent="j" id="S0p-oC-mLd"> + <connections> + <action selector="centerSelectionInVisibleArea:" target="-1" id="IOG-6D-g5B"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="Spelling and Grammar" id="Dv1-io-Yv7"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Spelling" id="3IN-sU-3Bg"> + <items> + <menuItem title="Show Spelling and Grammar" keyEquivalent=":" id="HFo-cy-zxI"> + <connections> + <action selector="showGuessPanel:" target="-1" id="vFj-Ks-hy3"/> + </connections> + </menuItem> + <menuItem title="Check Document Now" keyEquivalent=";" id="hz2-CU-CR7"> + <connections> + <action selector="checkSpelling:" target="-1" id="fz7-VC-reM"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="bNw-od-mp5"/> + <menuItem title="Check Spelling While Typing" id="rbD-Rh-wIN"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleContinuousSpellChecking:" target="-1" id="7w6-Qz-0kB"/> + </connections> + </menuItem> + <menuItem title="Check Grammar With Spelling" id="mK6-2p-4JG"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleGrammarChecking:" target="-1" id="muD-Qn-j4w"/> + </connections> + </menuItem> + <menuItem title="Correct Spelling Automatically" id="78Y-hA-62v"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleAutomaticSpellingCorrection:" target="-1" id="2lM-Qi-WAP"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="Substitutions" id="9ic-FL-obx"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Substitutions" id="FeM-D8-WVr"> + <items> + <menuItem title="Show Substitutions" id="z6F-FW-3nz"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="orderFrontSubstitutionsPanel:" target="-1" id="oku-mr-iSq"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="gPx-C9-uUO"/> + <menuItem title="Smart Copy/Paste" id="9yt-4B-nSM"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleSmartInsertDelete:" target="-1" id="3IJ-Se-DZD"/> + </connections> + </menuItem> + <menuItem title="Smart Quotes" id="hQb-2v-fYv"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleAutomaticQuoteSubstitution:" target="-1" id="ptq-xd-QOA"/> + </connections> + </menuItem> + <menuItem title="Smart Dashes" id="rgM-f4-ycn"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleAutomaticDashSubstitution:" target="-1" id="oCt-pO-9gS"/> + </connections> + </menuItem> + <menuItem title="Smart Links" id="cwL-P1-jid"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleAutomaticLinkDetection:" target="-1" id="Gip-E3-Fov"/> + </connections> + </menuItem> + <menuItem title="Data Detectors" id="tRr-pd-1PS"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleAutomaticDataDetection:" target="-1" id="R1I-Nq-Kbl"/> + </connections> + </menuItem> + <menuItem title="Text Replacement" id="HFQ-gK-NFA"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleAutomaticTextReplacement:" target="-1" id="DvP-Fe-Py6"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="Transformations" id="2oI-Rn-ZJC"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Transformations" id="c8a-y6-VQd"> + <items> + <menuItem title="Make Upper Case" id="vmV-6d-7jI"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="uppercaseWord:" target="-1" id="sPh-Tk-edu"/> + </connections> + </menuItem> + <menuItem title="Make Lower Case" id="d9M-CD-aMd"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="lowercaseWord:" target="-1" id="iUZ-b5-hil"/> + </connections> + </menuItem> + <menuItem title="Capitalize" id="UEZ-Bs-lqG"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="capitalizeWord:" target="-1" id="26H-TL-nsh"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="Speech" id="xrE-MZ-jX0"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Speech" id="3rS-ZA-NoH"> + <items> + <menuItem title="Start Speaking" id="Ynk-f8-cLZ"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="startSpeaking:" target="-1" id="654-Ng-kyl"/> + </connections> + </menuItem> + <menuItem title="Stop Speaking" id="Oyz-dy-DGm"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="stopSpeaking:" target="-1" id="dX8-6p-jy9"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="View" id="H8h-7b-M4v"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="View" id="HyV-fh-RgO"> + <items> + <menuItem title="Streets" state="on" tag="1" keyEquivalent="1" id="17N-yz-NNo"> + <connections> + <action selector="setStyle:" target="-1" id="I4L-Wx-UXA"/> + </connections> + </menuItem> + <menuItem title="Outdoors" tag="2" keyEquivalent="2" id="BBa-Qa-SQr"> + <connections> + <action selector="setStyle:" target="-1" id="rM1-yG-t5u"/> + </connections> + </menuItem> + <menuItem title="Light" tag="3" keyEquivalent="3" id="HWe-7u-UVJ"> + <connections> + <action selector="setStyle:" target="-1" id="Q9V-O1-oRz"/> + </connections> + </menuItem> + <menuItem title="Dark" tag="4" keyEquivalent="4" id="6HI-q6-AeV"> + <connections> + <action selector="setStyle:" target="-1" id="YfH-1I-G50"/> + </connections> + </menuItem> + <menuItem title="Satellite" tag="5" keyEquivalent="5" id="h0J-5X-kgF"> + <connections> + <action selector="setStyle:" target="-1" id="GXt-oK-Hy1"/> + </connections> + </menuItem> + <menuItem title="Satellite Streets" tag="6" keyEquivalent="6" id="9BL-00-HFt"> + <connections> + <action selector="setStyle:" target="-1" id="oL4-AC-waq"/> + </connections> + </menuItem> + <menuItem title="Custom Style…" id="L0h-86-2cU"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="chooseCustomStyle:" target="-1" id="QJF-fM-Ty3"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="BMF-ml-0Bd"/> + <menuItem title="Zoom In" keyEquivalent="+" id="W82-WO-xvB"> + <connections> + <action selector="zoomIn:" target="-1" id="g33-vK-zUu"/> + </connections> + </menuItem> + <menuItem title="Zoom Out" keyEquivalent="-" id="j7h-PY-edM"> + <connections> + <action selector="zoomOut:" target="-1" id="0pP-tO-9ex"/> + </connections> + </menuItem> + <menuItem title="Snap to North" keyEquivalent="" id="Zss-3w-wkz"> + <connections> + <action selector="snapToNorth:" target="-1" id="Ayq-GE-Lb5"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="mkP-YN-G0w"/> + <menuItem title="Reload" keyEquivalent="r" id="JvI-nv-KaE"> + <connections> + <action selector="reload:" target="-1" id="xkh-9F-mOe"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="CyM-Wv-Bnc"/> + <menuItem title="Show Toolbar" keyEquivalent="t" id="snW-S8-Cw5"> + <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/> + <connections> + <action selector="toggleToolbarShown:" target="-1" id="BXY-wc-z0C"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="Debug" id="ZNC-5r-eBw"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Debug" id="McE-ka-r79"> + <items> + <menuItem title="Show Tile Boundaries" id="rDE-dG-rTR"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleTileBoundaries:" target="-1" id="LAO-88-F7h"/> + </connections> + </menuItem> + <menuItem title="Show Tile Info" id="LoH-qD-kb0"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleTileInfo:" target="-1" id="KCn-0G-V87"/> + </connections> + </menuItem> + <menuItem title="Show Tile Timestamps" id="bY0-2E-LZ7"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleTileTimestamps:" target="-1" id="tBs-2N-KEG"/> + </connections> + </menuItem> + <menuItem title="Show Collision Boxes" id="Y0b-3K-mJE"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleCollisionBoxes:" target="-1" id="EYa-7n-iWZ"/> + </connections> + </menuItem> + <menuItem title="Show Wireframes" id="hSX-Be-8xC"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleWireframes:" target="-1" id="usj-ug-upt"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="2EG-Hp-4FA"/> + <menuItem title="Show Color Buffer" id="Eao-WE-BWz"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="showColorBuffer:" target="-1" id="Nuq-Qs-98g"/> + </connections> + </menuItem> + <menuItem title="Show Stencil Buffer" id="LlS-Yh-RkN"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="showStencilBuffer:" target="-1" id="WkN-t9-Mpv"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="dYw-bb-tr1"/> + <menuItem title="Show Tooltips on Dropped Pins" id="uir-Rx-zmw"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleShowsToolTipsOnDroppedPins:" target="-1" id="1YC-Co-QQ6"/> + </connections> + </menuItem> + <menuItem title="Use Random Cursors for Dropped Pins" id="ZTk-lc-Jgu"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleRandomizesCursorsOnDroppedPins:" target="-1" id="Mpw-b8-oub"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="Sl5-nE-kHd"/> + <menuItem title="Blanket Map With Pins" id="LMZ-oe-Ngh"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="dropManyPins:" target="-1" id="Rtv-8N-3Z8"/> + </connections> + </menuItem> + <menuItem title="Add Polygon and Polyline" id="DVr-vT-lpe"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="drawPolygonAndPolyLineAnnotations:" target="-1" id="EhT-CB-gee"/> + </connections> + </menuItem> + <menuItem title="Remove All Annotations" id="6rC-68-vk0"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="removeAllAnnotations:" target="-1" id="6v3-0E-LsR"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="wQq-Mx-QY0"/> + <menuItem title="Start World Tour" id="VFo-Jh-2sw"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="startWorldTour:" target="-1" id="66Y-Gm-Yn1"/> + </connections> + </menuItem> + <menuItem title="Stop World Tour" id="Pa8-qU-xfr"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="stopWorldTour:" target="-1" id="aq0-7t-AGi"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="Window" id="aUF-d1-5bR"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Window" systemMenu="window" id="Td7-aD-5lo"> + <items> + <menuItem title="Minimize" keyEquivalent="m" id="OY7-WF-poV"> + <connections> + <action selector="performMiniaturize:" target="-1" id="VwT-WD-YPe"/> + </connections> + </menuItem> + <menuItem title="Zoom" keyEquivalent="z" id="R4o-n2-Eq4"> + <modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/> + <connections> + <action selector="performZoom:" target="-1" id="DIl-cC-cCs"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="Uix-g7-fAt"/> + <menuItem title="Offline Packs" id="YW3-jR-knj"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="showOfflinePacksPanel:" target="Voe-Tx-rLC" id="kj9-ht-KmF"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="eu3-7i-yIM"/> + <menuItem title="Bring All to Front" id="LE2-aR-0XJ"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="arrangeInFront:" target="-1" id="DRN-fu-gQh"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="Help" id="wpr-3q-Mcd"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Help" systemMenu="help" id="F2S-fz-NVQ"> + <items> + <menuItem title="Mapbox GL Help" keyEquivalent="?" id="FKE-Sm-Kum"> + <connections> + <action selector="showShortcuts:" target="-1" id="hNZ-sm-X2q"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="EpY-wQ-SjH"/> + <menuItem title="Improve This Map" id="xu5-WN-qYK"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="giveFeedback:" target="-1" id="cil-i9-r39"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + </items> + </menu> + <window title="Preferences" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" oneShot="NO" releasedWhenClosed="NO" showsToolbarButton="NO" visibleAtLaunch="NO" frameAutosaveName="Preferences" animationBehavior="default" id="UWc-yQ-qda"> + <windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES"/> + <windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/> + <rect key="contentRect" x="109" y="131" width="350" height="84"/> + <rect key="screenRect" x="0.0" y="0.0" width="1280" height="777"/> + <view key="contentView" id="eA4-n3-qPe"> + <rect key="frame" x="0.0" y="0.0" width="350" height="84"/> + <autoresizingMask key="autoresizingMask"/> + <subviews> + <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="0IK-AW-Gg3"> + <rect key="frame" x="18" y="45" width="89" height="17"/> + <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Access token:" id="Ptd-FI-M5A"> + <font key="font" metaFont="system"/> + <color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/> + <color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/> + </textFieldCell> + <connections> + <accessibilityConnection property="link" destination="7sb-sf-oJU" id="U0t-jC-oQ7"/> + </connections> + </textField> + <textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="7sb-sf-oJU"> + <rect key="frame" x="113" y="42" width="197" height="22"/> + <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" state="on" borderStyle="bezel" drawsBackground="YES" id="jlV-TC-NUv"> + <font key="font" metaFont="system"/> + <color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/> + <color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/> + </textFieldCell> + <connections> + <binding destination="45S-yT-WUN" name="value" keyPath="values.MGLMapboxAccessToken" id="iJE-S2-ALY"/> + </connections> + </textField> + <button translatesAutoresizingMaskIntoConstraints="NO" id="c3S-LC-PoX"> + <rect key="frame" x="318" y="47" width="12" height="12"/> + <constraints> + <constraint firstAttribute="width" constant="12" id="M3J-pU-gKn"/> + </constraints> + <buttonCell key="cell" type="square" bezelStyle="shadowlessSquare" image="NSFollowLinkFreestandingTemplate" imagePosition="only" alignment="center" controlSize="small" imageScaling="proportionallyUpOrDown" inset="2" id="38x-37-Ay0"> + <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/> + <font key="font" metaFont="smallSystem"/> + </buttonCell> + <connections> + <action selector="openAccessTokenManager:" target="-1" id="1LX-4G-roC"/> + </connections> + </button> + <button translatesAutoresizingMaskIntoConstraints="NO" id="7IZ-zl-iT1"> + <rect key="frame" x="18" y="18" width="109" height="18"/> + <buttonCell key="cell" type="check" title="Scroll to zoom" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="hVR-66-JSh"> + <behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/> + <font key="font" metaFont="system"/> + </buttonCell> + <connections> + <binding destination="45S-yT-WUN" name="value" keyPath="values.MGLScrollWheelZoomsMapView" id="2AZ-bk-DM5"/> + </connections> + </button> + </subviews> + <constraints> + <constraint firstAttribute="trailing" secondItem="c3S-LC-PoX" secondAttribute="trailing" constant="20" symbolic="YES" id="7QU-Jd-Rg6"/> + <constraint firstItem="c3S-LC-PoX" firstAttribute="top" secondItem="eA4-n3-qPe" secondAttribute="top" constant="25" id="JOS-HU-27c"/> + <constraint firstItem="7sb-sf-oJU" firstAttribute="leading" secondItem="0IK-AW-Gg3" secondAttribute="trailing" constant="8" symbolic="YES" id="SS6-VQ-sLK"/> + <constraint firstItem="0IK-AW-Gg3" firstAttribute="leading" secondItem="eA4-n3-qPe" secondAttribute="leading" constant="20" symbolic="YES" id="TYG-io-qfV"/> + <constraint firstItem="7sb-sf-oJU" firstAttribute="top" secondItem="eA4-n3-qPe" secondAttribute="top" constant="20" symbolic="YES" id="Vzb-q8-ecP"/> + <constraint firstItem="7IZ-zl-iT1" firstAttribute="leading" secondItem="0IK-AW-Gg3" secondAttribute="leading" id="aIY-WX-AW9"/> + <constraint firstItem="7IZ-zl-iT1" firstAttribute="top" secondItem="7sb-sf-oJU" secondAttribute="bottom" constant="8" symbolic="YES" id="ide-24-GqL"/> + <constraint firstItem="c3S-LC-PoX" firstAttribute="leading" secondItem="7sb-sf-oJU" secondAttribute="trailing" constant="8" symbolic="YES" id="pjl-9u-IgM"/> + <constraint firstItem="7sb-sf-oJU" firstAttribute="baseline" secondItem="0IK-AW-Gg3" secondAttribute="baseline" id="qIY-Jr-9Ws"/> + <constraint firstAttribute="bottom" secondItem="7IZ-zl-iT1" secondAttribute="bottom" constant="20" symbolic="YES" id="wng-pn-VIz"/> + <constraint firstItem="7sb-sf-oJU" firstAttribute="centerY" secondItem="c3S-LC-PoX" secondAttribute="centerY" id="zej-gw-fC0"/> + </constraints> + </view> + <connections> + <outlet property="initialFirstResponder" destination="7sb-sf-oJU" id="UZe-di-dnA"/> + </connections> + <point key="canvasLocation" x="754" y="221"/> + </window> + <userDefaultsController representsSharedInstance="YES" id="45S-yT-WUN"/> + <window title="Offline Packs" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" hidesOnDeactivate="YES" oneShot="NO" releasedWhenClosed="NO" showsToolbarButton="NO" visibleAtLaunch="NO" frameAutosaveName="MBXOfflinePacksPanel" animationBehavior="default" id="Jjv-gs-Tx6" customClass="NSPanel"> + <windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES" utility="YES"/> + <windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/> + <rect key="contentRect" x="830" y="430" width="400" height="300"/> + <rect key="screenRect" x="0.0" y="0.0" width="1280" height="777"/> + <view key="contentView" id="8ha-hw-zOD"> + <rect key="frame" x="0.0" y="0.0" width="400" height="300"/> + <autoresizingMask key="autoresizingMask"/> + <subviews> + <scrollView autohidesScrollers="YES" horizontalLineScroll="19" horizontalPageScroll="10" verticalLineScroll="19" verticalPageScroll="10" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Q8b-0e-dLv"> + <rect key="frame" x="-1" y="20" width="402" height="281"/> + <clipView key="contentView" id="J9U-Yx-o2S"> + <rect key="frame" x="1" y="0.0" width="400" height="280"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> + <subviews> + <tableView verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnAutoresizingStyle="lastColumnOnly" autosaveColumns="NO" headerView="MAZ-Iq-hBi" id="Ato-Vu-HYT"> + <rect key="frame" x="0.0" y="0.0" width="423" height="257"/> + <autoresizingMask key="autoresizingMask"/> + <size key="intercellSpacing" width="3" height="2"/> + <color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/> + <color key="gridColor" name="gridColor" catalog="System" colorSpace="catalog"/> + <tableColumns> + <tableColumn identifier="" editable="NO" width="16" minWidth="10" maxWidth="3.4028234663852886e+38" id="xtw-hQ-8C5" userLabel="State"> + <tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="left"> + <font key="font" metaFont="smallSystem"/> + <color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/> + <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/> + </tableHeaderCell> + <imageCell key="dataCell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" id="edU-Yw-20f"/> + <tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/> + <connections> + <binding destination="dWe-R6-sRz" name="value" keyPath="arrangedObjects.stateImage" id="2wd-1J-TZt"/> + </connections> + </tableColumn> + <tableColumn editable="NO" width="116" minWidth="40" maxWidth="1000" id="2hD-LN-h0L"> + <tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" title="Name"> + <font key="font" metaFont="smallSystem"/> + <color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/> + <color key="backgroundColor" name="headerColor" catalog="System" colorSpace="catalog"/> + </tableHeaderCell> + <textFieldCell key="dataCell" lineBreakMode="truncatingTail" selectable="YES" editable="YES" title="Text Cell" id="oys-QZ-34I"> + <font key="font" metaFont="system"/> + <color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/> + <color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/> + </textFieldCell> + <tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/> + <connections> + <binding destination="dWe-R6-sRz" name="value" keyPath="arrangedObjects.context" id="NtD-s5-ZUq"> + <dictionary key="options"> + <string key="NSValueTransformerName">OfflinePackNameValueTransformer</string> + </dictionary> + </binding> + </connections> + </tableColumn> + <tableColumn editable="NO" width="50" minWidth="40" maxWidth="1000" id="pkI-c7-xoD"> + <tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" title="Downloaded Resources"> + <font key="font" metaFont="smallSystem"/> + <color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/> + <color key="backgroundColor" name="headerColor" catalog="System" colorSpace="catalog"/> + </tableHeaderCell> + <textFieldCell key="dataCell" lineBreakMode="truncatingTail" selectable="YES" editable="YES" title="Text Cell" id="WfC-qb-HsW"> + <numberFormatter key="formatter" formatterBehavior="default10_4" numberStyle="decimal" minimumIntegerDigits="1" maximumIntegerDigits="2000000000" id="sNm-Qn-ne6"/> + <font key="font" metaFont="system"/> + <color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/> + <color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/> + </textFieldCell> + <tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/> + <connections> + <binding destination="dWe-R6-sRz" name="value" keyPath="arrangedObjects.countOfResourcesCompleted" id="mu6-Jg-GiU"/> + </connections> + </tableColumn> + <tableColumn identifier="" editable="NO" width="50" minWidth="10" maxWidth="3.4028234663852886e+38" id="Rrd-A9-jqc"> + <tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="left" title="Total Resources"> + <font key="font" metaFont="smallSystem"/> + <color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/> + <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/> + </tableHeaderCell> + <textFieldCell key="dataCell" lineBreakMode="truncatingTail" selectable="YES" editable="YES" alignment="left" title="Text Cell" id="mHy-qJ-rOA"> + <numberFormatter key="formatter" formatterBehavior="default10_4" numberStyle="decimal" minimumIntegerDigits="1" maximumIntegerDigits="2000000000" id="kyx-ZP-OBH"/> + <font key="font" metaFont="system"/> + <color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/> + <color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/> + </textFieldCell> + <tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/> + <connections> + <binding destination="dWe-R6-sRz" name="value" keyPath="arrangedObjects.countOfResourcesExpected" id="mh2-k0-vvB"/> + </connections> + </tableColumn> + <tableColumn editable="NO" width="50" minWidth="40" maxWidth="1000" id="kCO-Cd-bQt"> + <tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" title="Downloaded Tiles"> + <font key="font" metaFont="smallSystem"/> + <color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/> + <color key="backgroundColor" name="headerColor" catalog="System" colorSpace="catalog"/> + </tableHeaderCell> + <textFieldCell key="dataCell" lineBreakMode="truncatingTail" selectable="YES" editable="YES" title="Text Cell" id="hUl-2C-sHr"> + <numberFormatter key="formatter" formatterBehavior="default10_4" numberStyle="decimal" minimumIntegerDigits="1" maximumIntegerDigits="2000000000" id="KjY-J1-gSm"/> + <font key="font" metaFont="system"/> + <color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/> + <color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/> + </textFieldCell> + <tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/> + <connections> + <binding destination="dWe-R6-sRz" name="value" keyPath="arrangedObjects.countOfTilesCompleted" id="XHn-D7-zqf"> + <dictionary key="options"> + <bool key="NSConditionallySetsEditable" value="YES"/> + </dictionary> + </binding> + </connections> + </tableColumn> + <tableColumn editable="NO" width="60" minWidth="10" maxWidth="3.4028234663852886e+38" id="WO5-Ci-HgG"> + <tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="left" title="Downloaded Tiles Size"> + <font key="font" metaFont="smallSystem"/> + <color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/> + <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/> + </tableHeaderCell> + <textFieldCell key="dataCell" lineBreakMode="truncatingTail" selectable="YES" editable="YES" alignment="left" title="Text Cell" id="cKy-kF-5Pv"> + <byteCountFormatter key="formatter" allowsNonnumericFormatting="NO" includesActualByteCount="YES" id="bHS-Ch-aXU"/> + <font key="font" metaFont="system"/> + <color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/> + <color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/> + </textFieldCell> + <tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/> + <connections> + <binding destination="dWe-R6-sRz" name="value" keyPath="arrangedObjects.countOfTileBytesCompleted" id="Xpk-BZ-Xcr"> + <dictionary key="options"> + <bool key="NSConditionallySetsEditable" value="YES"/> + </dictionary> + </binding> + </connections> + </tableColumn> + <tableColumn identifier="" editable="NO" width="60" minWidth="10" maxWidth="3.4028234663852886e+38" id="h7m-6l-KaS"> + <tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="left" title="Downloaded Resources Size"> + <font key="font" metaFont="smallSystem"/> + <color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/> + <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/> + </tableHeaderCell> + <textFieldCell key="dataCell" lineBreakMode="truncatingTail" selectable="YES" editable="YES" alignment="left" title="Text Cell" id="701-bg-k6L"> + <byteCountFormatter key="formatter" allowsNonnumericFormatting="NO" includesActualByteCount="YES" id="IXV-J9-sP3"/> + <font key="font" metaFont="system"/> + <color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/> + <color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/> + </textFieldCell> + <tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/> + <connections> + <binding destination="dWe-R6-sRz" name="value" keyPath="arrangedObjects.countOfBytesCompleted" id="Zsa-Na-yFN"/> + </connections> + </tableColumn> + </tableColumns> + <connections> + <action trigger="doubleAction" selector="chooseOfflinePack:" target="-1" id="pUN-eT-zRT"/> + </connections> + </tableView> + </subviews> + <color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/> + </clipView> + <scroller key="horizontalScroller" verticalHuggingPriority="750" horizontal="YES" id="QLr-6P-Ogs"> + <rect key="frame" x="1" y="264" width="400" height="16"/> + <autoresizingMask key="autoresizingMask"/> + </scroller> + <scroller key="verticalScroller" hidden="YES" verticalHuggingPriority="750" horizontal="NO" id="q0K-eE-mzL"> + <rect key="frame" x="224" y="17" width="15" height="102"/> + <autoresizingMask key="autoresizingMask"/> + </scroller> + <tableHeaderView key="headerView" id="MAZ-Iq-hBi"> + <rect key="frame" x="0.0" y="0.0" width="423" height="23"/> + <autoresizingMask key="autoresizingMask"/> + </tableHeaderView> + </scrollView> + <button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="wzf-ce-Spm"> + <rect key="frame" x="0.0" y="-1" width="21" height="21"/> + <constraints> + <constraint firstAttribute="width" constant="21" id="5ST-tY-8Ph"/> + </constraints> + <buttonCell key="cell" type="smallSquare" bezelStyle="smallSquare" image="NSAddTemplate" imagePosition="overlaps" alignment="center" lineBreakMode="truncatingTail" state="on" imageScaling="proportionallyDown" inset="2" id="sew-F7-i5T"> + <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/> + <font key="font" metaFont="system"/> + </buttonCell> + <connections> + <action selector="addOfflinePack:" target="-1" id="SN0-PM-HoU"/> + </connections> + </button> + <button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="7L7-hr-zId"> + <rect key="frame" x="20" y="0.0" width="21" height="19"/> + <constraints> + <constraint firstAttribute="width" constant="21" id="JYb-AF-8gZ"/> + </constraints> + <buttonCell key="cell" type="smallSquare" bezelStyle="smallSquare" image="NSRemoveTemplate" imagePosition="overlaps" alignment="center" lineBreakMode="truncatingTail" state="on" imageScaling="proportionallyDown" inset="2" id="oTF-3m-6qT"> + <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/> + <font key="font" metaFont="system"/> + <string key="keyEquivalent" base64-UTF8="YES"> +CA +</string> + </buttonCell> + <connections> + <action selector="delete:" target="-1" id="EGL-bf-yUD"/> + </connections> + </button> + </subviews> + <constraints> + <constraint firstItem="7L7-hr-zId" firstAttribute="centerY" secondItem="wzf-ce-Spm" secondAttribute="centerY" id="7TI-6w-bf1"/> + <constraint firstAttribute="bottom" secondItem="Q8b-0e-dLv" secondAttribute="bottom" constant="20" symbolic="YES" id="DZa-ly-bhV"/> + <constraint firstItem="wzf-ce-Spm" firstAttribute="top" secondItem="Q8b-0e-dLv" secondAttribute="bottom" id="LhK-5z-CQA"/> + <constraint firstItem="Q8b-0e-dLv" firstAttribute="leading" secondItem="8ha-hw-zOD" secondAttribute="leading" constant="-1" id="Oyo-ch-rZo"/> + <constraint firstAttribute="bottom" secondItem="7L7-hr-zId" secondAttribute="bottom" id="TtY-j1-T5h"/> + <constraint firstItem="Q8b-0e-dLv" firstAttribute="top" secondItem="8ha-hw-zOD" secondAttribute="top" constant="-1" id="WDk-Ig-Grr"/> + <constraint firstAttribute="trailing" secondItem="Q8b-0e-dLv" secondAttribute="trailing" constant="-1" id="hHf-rd-Wcv"/> + <constraint firstItem="7L7-hr-zId" firstAttribute="leading" secondItem="8ha-hw-zOD" secondAttribute="leading" constant="20" symbolic="YES" id="iKJ-ph-ACS"/> + <constraint firstAttribute="bottom" secondItem="wzf-ce-Spm" secondAttribute="bottom" constant="-1" id="jFV-Xi-fWr"/> + <constraint firstItem="wzf-ce-Spm" firstAttribute="leading" secondItem="8ha-hw-zOD" secondAttribute="leading" id="kJt-oJ-72R"/> + </constraints> + </view> + <point key="canvasLocation" x="720" y="317"/> + </window> + <arrayController objectClassName="MGLOfflinePack" editable="NO" avoidsEmptySelection="NO" id="dWe-R6-sRz" userLabel="Offline Packs Array Controller"> + <declaredKeys> + <string>context</string> + <string>countOfResourcesCompleted</string> + <string>countOfResourcesExpected</string> + <string>countOfTilesCompleted</string> + <string>countOfTileBytesCompleted</string> + <string>countOfBytesCompleted</string> + <string>stateImage</string> + </declaredKeys> + </arrayController> + </objects> + <resources> + <image name="NSAddTemplate" width="11" height="11"/> + <image name="NSFollowLinkFreestandingTemplate" width="14" height="14"/> + <image name="NSRemoveTemplate" width="11" height="11"/> + </resources> +</document> diff --git a/platform/macos/app/Base.lproj/MapDocument.xib b/platform/macos/app/Base.lproj/MapDocument.xib new file mode 100644 index 0000000000..55d82d21d0 --- /dev/null +++ b/platform/macos/app/Base.lproj/MapDocument.xib @@ -0,0 +1,142 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES"> + <dependencies> + <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="10117"/> + </dependencies> + <objects> + <customObject id="-2" userLabel="File's Owner" customClass="MapDocument"> + <connections> + <outlet property="mapView" destination="q4d-kF-8Hi" id="7hI-dS-A5R"/> + <outlet property="mapViewContextMenu" destination="XbX-6a-Mgy" id="YD0-1r-5N2"/> + <outlet property="window" destination="cSv-fg-MAQ" id="TBu-Mu-79N"/> + </connections> + </customObject> + <customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/> + <customObject id="-3" userLabel="Application" customClass="NSObject"/> + <window allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" frameAutosaveName="MBXMapWindow" animationBehavior="default" id="cSv-fg-MAQ"> + <windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES" fullSizeContentView="YES"/> + <windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/> + <rect key="contentRect" x="388" y="211" width="512" height="480"/> + <rect key="screenRect" x="0.0" y="0.0" width="1280" height="777"/> + <view key="contentView" id="TuG-C5-zLS"> + <rect key="frame" x="0.0" y="0.0" width="512" height="480"/> + <autoresizingMask key="autoresizingMask"/> + <subviews> + <customView translatesAutoresizingMaskIntoConstraints="NO" id="q4d-kF-8Hi" customClass="MGLMapView"> + <rect key="frame" x="0.0" y="0.0" width="512" height="480"/> + <connections> + <outlet property="delegate" destination="-2" id="dh2-0H-jFZ"/> + <outlet property="menu" destination="XbX-6a-Mgy" id="dSu-HR-Kq2"/> + </connections> + </customView> + </subviews> + <constraints> + <constraint firstAttribute="bottom" secondItem="q4d-kF-8Hi" secondAttribute="bottom" id="L2t-Be-qWL"/> + <constraint firstItem="q4d-kF-8Hi" firstAttribute="top" secondItem="TuG-C5-zLS" secondAttribute="top" id="T8A-o3-Bhq"/> + <constraint firstItem="q4d-kF-8Hi" firstAttribute="leading" secondItem="TuG-C5-zLS" secondAttribute="leading" id="fGH-YW-Qd3"/> + <constraint firstAttribute="trailing" secondItem="q4d-kF-8Hi" secondAttribute="trailing" id="yfG-iG-K4C"/> + </constraints> + </view> + <toolbar key="toolbar" implicitIdentifier="A3AC6577-4712-4628-813D-113498171A84" allowsUserCustomization="NO" displayMode="iconOnly" sizeMode="regular" id="DTc-AP-Bah"> + <allowedToolbarItems> + <toolbarItem implicitItemIdentifier="NSToolbarSpaceItem" id="bld-8W-Wgg"/> + <toolbarItem implicitItemIdentifier="NSToolbarFlexibleSpaceItem" id="z4l-5x-MzK"/> + <toolbarItem implicitItemIdentifier="2CB58C0A-7B95-4233-8DD3-F94BFE7D3061" label="Share" paletteLabel="Share" image="NSShareTemplate" id="XJT-Ho-tuZ" customClass="ValidatedToolbarItem"> + <nil key="toolTip"/> + <size key="minSize" width="40" height="32"/> + <size key="maxSize" width="48" height="32"/> + <button key="view" verticalHuggingPriority="750" id="y6e-ev-rVL"> + <rect key="frame" x="0.0" y="14" width="48" height="32"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> + <buttonCell key="cell" type="roundTextured" bezelStyle="texturedRounded" image="NSShareTemplate" imagePosition="only" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="TBK-Ra-XzZ"> + <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/> + <font key="font" metaFont="system"/> + </buttonCell> + </button> + <connections> + <action selector="showShareMenu:" target="-1" id="fCB-HP-iou"/> + </connections> + </toolbarItem> + <toolbarItem implicitItemIdentifier="BA3542AF-D63A-4893-9CC7-8F67EF2E82B0" label="Style" paletteLabel="Style" id="u23-0z-Otl" customClass="ValidatedToolbarItem"> + <nil key="toolTip"/> + <size key="minSize" width="100" height="26"/> + <size key="maxSize" width="120" height="26"/> + <popUpButton key="view" verticalHuggingPriority="750" id="Tzm-Cy-dQg"> + <rect key="frame" x="0.0" y="14" width="120" height="26"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> + <popUpButtonCell key="cell" type="roundTextured" title="Streets" bezelStyle="texturedRounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="border" tag="1" imageScaling="proportionallyDown" inset="2" selectedItem="wvt-tP-O3a" id="3PJ-qK-Oh3"> + <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/> + <font key="font" metaFont="menu"/> + <menu key="menu" id="xf3-qk-IhF"> + <items> + <menuItem title="Streets" state="on" tag="1" id="wvt-tP-O3a"/> + <menuItem title="Outdoors" tag="2" id="RkE-lp-fL9"/> + <menuItem title="Light" tag="3" id="R4X-kt-HHb"/> + <menuItem title="Dark" tag="4" id="jUC-5X-0Zx"> + <modifierMask key="keyEquivalentModifierMask"/> + </menuItem> + <menuItem title="Satellite" tag="5" id="CTe-e2-o42"> + <modifierMask key="keyEquivalentModifierMask"/> + </menuItem> + <menuItem title="Satellite Streets" tag="6" id="7ly-oA-0ND"> + <modifierMask key="keyEquivalentModifierMask"/> + </menuItem> + </items> + </menu> + </popUpButtonCell> + </popUpButton> + <connections> + <action selector="setStyle:" target="-1" id="2Kw-9i-a3G"/> + </connections> + </toolbarItem> + </allowedToolbarItems> + <defaultToolbarItems> + <toolbarItem reference="XJT-Ho-tuZ"/> + <toolbarItem reference="z4l-5x-MzK"/> + <toolbarItem reference="u23-0z-Otl"/> + </defaultToolbarItems> + <connections> + <outlet property="delegate" destination="-2" id="V9D-gS-Tvu"/> + </connections> + </toolbar> + <connections> + <binding destination="-2" name="displayPatternTitle1" keyPath="mapView.centerCoordinate" id="wtz-AV-bG1"> + <dictionary key="options"> + <string key="NSDisplayPattern">%{title1}@</string> + <string key="NSValueTransformerName">LocationCoordinate2DTransformer</string> + </dictionary> + </binding> + <outlet property="delegate" destination="-2" id="HEo-Qf-o6o"/> + </connections> + </window> + <menu title="Map View" id="XbX-6a-Mgy"> + <items> + <menuItem title="Drop Pin" id="qZJ-mM-bLj"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="dropPin:" target="-1" id="hxx-eC-kqU"/> + </connections> + </menuItem> + <menuItem title="Remove Pin" id="Zhx-30-VmE"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="removePin:" target="-1" id="w0R-0B-7mG"/> + </connections> + </menuItem> + <menuItem title="Select Features" id="za5-bY-mdf"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="selectFeatures:" target="-1" id="ikt-CZ-yZT"/> + </connections> + </menuItem> + </items> + <connections> + <outlet property="delegate" destination="-2" id="oHe-ZP-lyc"/> + </connections> + <point key="canvasLocation" x="820" y="254.5"/> + </menu> + </objects> + <resources> + <image name="NSShareTemplate" width="11" height="16"/> + </resources> +</document> diff --git a/platform/macos/app/Credits.rtf b/platform/macos/app/Credits.rtf new file mode 100644 index 0000000000..6b17eb34b2 --- /dev/null +++ b/platform/macos/app/Credits.rtf @@ -0,0 +1,9 @@ +{\rtf1\ansi\ansicpg1252\cocoartf1404\cocoasubrtf130 +{\fonttbl\f0\fnil\fcharset0 SFUIText-Regular;} +{\colortbl;\red255\green255\blue255;} +\margl1440\margr1440\vieww10800\viewh8400\viewkind0 +\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 + +\f0\fs20 \cf0 Copyright \'a9 {\field{\*\fldinst{HYPERLINK "https://www.mapbox.com/about/maps/"}}{\fldrslt Mapbox}}.\ +\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 +\cf0 Copyright \'a9 {\field{\*\fldinst{HYPERLINK "http://www.openstreetmap.org/about/"}}{\fldrslt OpenStreetMap contributors}}.}
\ No newline at end of file diff --git a/platform/macos/app/DroppedPinAnnotation.h b/platform/macos/app/DroppedPinAnnotation.h new file mode 100644 index 0000000000..435a56738b --- /dev/null +++ b/platform/macos/app/DroppedPinAnnotation.h @@ -0,0 +1,10 @@ +#import <Mapbox/Mapbox.h> + +@interface DroppedPinAnnotation : MGLPointAnnotation + +@property (nonatomic, readonly) NSTimeInterval elapsedShownTime; + +- (void)resume; +- (void)pause; + +@end diff --git a/platform/macos/app/DroppedPinAnnotation.m b/platform/macos/app/DroppedPinAnnotation.m new file mode 100644 index 0000000000..5b19fd7401 --- /dev/null +++ b/platform/macos/app/DroppedPinAnnotation.m @@ -0,0 +1,68 @@ +#import "DroppedPinAnnotation.h" + +#import "LocationCoordinate2DTransformer.h" +#import "TimeIntervalTransformer.h" + +#import <Mapbox/Mapbox.h> + +static MGLCoordinateFormatter *DroppedPinCoordinateFormatter; + +@implementation DroppedPinAnnotation { + NSTimer *_timer; + NSTimeInterval _priorShownTimeInterval; + NSDate *_dateShown; + + NSValueTransformer *_timeIntervalTransformer; +} + ++ (void)initialize { + if (self == [DroppedPinAnnotation class]) { + DroppedPinCoordinateFormatter = [[MGLCoordinateFormatter alloc] init]; + } +} + +- (instancetype)init { + if (self = [super init]) { + _timeIntervalTransformer = [NSValueTransformer valueTransformerForName: + NSStringFromClass([TimeIntervalTransformer class])]; + [self update:nil]; + } + return self; +} + +- (void)dealloc { + [self pause]; +} + +- (void)setCoordinate:(CLLocationCoordinate2D)coordinate { + super.coordinate = coordinate; + [self update:nil]; +} + +- (NSTimeInterval)elapsedShownTime { + return _priorShownTimeInterval - _dateShown.timeIntervalSinceNow; +} + +- (void)resume { + _dateShown = [NSDate date]; + _timer = [NSTimer scheduledTimerWithTimeInterval:1 + target:self + selector:@selector(update:) + userInfo:nil + repeats:YES]; +} + +- (void)pause { + [_timer invalidate]; + _timer = nil; + _priorShownTimeInterval -= _dateShown.timeIntervalSinceNow; + _dateShown = nil; +} + +- (void)update:(NSTimer *)timer { + NSString *coordinate = [DroppedPinCoordinateFormatter stringFromCoordinate:self.coordinate]; + NSString *elapsedTime = [_timeIntervalTransformer transformedValue:@(self.elapsedShownTime)]; + self.subtitle = [NSString stringWithFormat:@"%@\nSelected for %@", coordinate, elapsedTime]; +} + +@end diff --git a/platform/macos/app/Info.plist b/platform/macos/app/Info.plist new file mode 100644 index 0000000000..cc7037f589 --- /dev/null +++ b/platform/macos/app/Info.plist @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleDevelopmentRegion</key> + <string>en</string> + <key>CFBundleDocumentTypes</key> + <array> + <dict> + <key>CFBundleTypeExtensions</key> + <array> + <string>mbx</string> + </array> + <key>CFBundleTypeName</key> + <string>Mapbox GL Map</string> + <key>CFBundleTypeRole</key> + <string>Editor</string> + <key>NSDocumentClass</key> + <string>MapDocument</string> + </dict> + </array> + <key>CFBundleExecutable</key> + <string>${EXECUTABLE_NAME}</string> + <key>CFBundleIdentifier</key> + <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundleName</key> + <string>${PRODUCT_NAME}</string> + <key>CFBundlePackageType</key> + <string>APPL</string> + <key>CFBundleShortVersionString</key> + <string>0.1.0</string> + <key>CFBundleSignature</key> + <string>MBGL</string> + <key>CFBundleURLTypes</key> + <array> + <dict> + <key>CFBundleURLName</key> + <string>${PRODUCT_NAME}</string> + <key>CFBundleURLSchemes</key> + <array> + <string>mapboxgl</string> + </array> + </dict> + </array> + <key>CFBundleVersion</key> + <string>1</string> + <key>LSMinimumSystemVersion</key> + <string>$(MACOSX_DEPLOYMENT_TARGET)</string> + <key>NSMainNibFile</key> + <string>MainMenu</string> + <key>NSPrincipalClass</key> + <string>NSApplication</string> +</dict> +</plist> diff --git a/platform/macos/app/LocationCoordinate2DTransformer.h b/platform/macos/app/LocationCoordinate2DTransformer.h new file mode 100644 index 0000000000..162325fbad --- /dev/null +++ b/platform/macos/app/LocationCoordinate2DTransformer.h @@ -0,0 +1,5 @@ +#import <Foundation/Foundation.h> + +@interface LocationCoordinate2DTransformer : NSValueTransformer + +@end diff --git a/platform/macos/app/LocationCoordinate2DTransformer.m b/platform/macos/app/LocationCoordinate2DTransformer.m new file mode 100644 index 0000000000..59654f1676 --- /dev/null +++ b/platform/macos/app/LocationCoordinate2DTransformer.m @@ -0,0 +1,31 @@ +#import "LocationCoordinate2DTransformer.h" + +#import <Mapbox/Mapbox.h> + +@implementation LocationCoordinate2DTransformer { + MGLCoordinateFormatter *_coordinateFormatter; +} + ++ (Class)transformedValueClass { + return [NSString class]; +} + ++ (BOOL)allowsReverseTransformation { + return NO; +} + +- (instancetype)init { + if (self = [super init]) { + _coordinateFormatter = [[MGLCoordinateFormatter alloc] init]; + } + return self; +} + +- (id)transformedValue:(id)value { + if (![value isKindOfClass:[NSValue class]]) { + return nil; + } + return [_coordinateFormatter stringForObjectValue:value].capitalizedString; +} + +@end diff --git a/platform/macos/app/MapDocument.h b/platform/macos/app/MapDocument.h new file mode 100644 index 0000000000..86ad05e6e2 --- /dev/null +++ b/platform/macos/app/MapDocument.h @@ -0,0 +1,14 @@ +#import <Cocoa/Cocoa.h> + +@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/macos/app/MapDocument.m b/platform/macos/app/MapDocument.m new file mode 100644 index 0000000000..e9f3b99592 --- /dev/null +++ b/platform/macos/app/MapDocument.m @@ -0,0 +1,778 @@ +#import "MapDocument.h" + +#import "AppDelegate.h" +#import "DroppedPinAnnotation.h" + +#import <Mapbox/Mapbox.h> + +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 }, +}; + +NS_ARRAY_OF(id <MGLAnnotation>) *MBXFlattenedShapes(NS_ARRAY_OF(id <MGLAnnotation>) *shapes) { + NSMutableArray *flattenedShapes = [NSMutableArray arrayWithCapacity:shapes.count]; + for (id <MGLAnnotation> shape in shapes) { + NSArray *subshapes; + // Flatten multipoints but not polylines or polygons. + if ([shape isMemberOfClass:[MGLMultiPoint class]]) { + NSUInteger pointCount = [(MGLMultiPoint *)shape pointCount]; + CLLocationCoordinate2D *coordinates = [(MGLMultiPoint *)shape coordinates]; + NSMutableArray *pointAnnotations = [NSMutableArray arrayWithCapacity:pointCount]; + for (NSUInteger i = 0; i < pointCount; i++) { + MGLPointAnnotation *pointAnnotation = [[MGLPointAnnotation alloc] init]; + pointAnnotation.coordinate = coordinates[i]; + [pointAnnotations addObject:pointAnnotation]; + } + subshapes = pointAnnotations; + } else if ([shape isKindOfClass:[MGLMultiPolyline class]]) { + subshapes = [(MGLMultiPolyline *)shape polylines]; + } else if ([shape isKindOfClass:[MGLMultiPolygon class]]) { + subshapes = [(MGLMultiPolygon *)shape polygons]; + } else if ([shape isKindOfClass:[MGLShapeCollection class]]) { + subshapes = MBXFlattenedShapes([(MGLShapeCollection *)shape shapes]); + } + + if (subshapes) { + [flattenedShapes addObjectsFromArray:subshapes]; + } else { + [flattenedShapes addObject:shape]; + } + } + return flattenedShapes; +} + +@interface MapDocument () <NSWindowDelegate, NSSharingServicePickerDelegate, NSMenuDelegate, MGLMapViewDelegate> + +@property (weak) IBOutlet NSMenu *mapViewContextMenu; + +@end + +@implementation MapDocument { + /// Style URL inherited from an existing document at the time this document + /// was created. + NSURL *_inheritedStyleURL; + + NSPoint _mouseLocationForMapViewContextMenu; + NSUInteger _droppedPinCounter; + NSNumberFormatter *_spellOutNumberFormatter; + + BOOL _showsToolTipsOnDroppedPins; + BOOL _randomizesCursorsOnDroppedPins; + BOOL _isTouringWorld; + BOOL _isShowingPolygonAndPolylineAnnotations; +} + +#pragma mark Lifecycle + +- (NSString *)windowNibName { + return @"MapDocument"; +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (void)windowControllerWillLoadNib:(NSWindowController *)windowController { + NSDocument *currentDocument = [NSDocumentController sharedDocumentController].currentDocument; + if ([currentDocument isKindOfClass:[MapDocument class]]) { + _inheritedStyleURL = [(MapDocument *)currentDocument mapView].styleURL; + } +} + +- (void)windowControllerDidLoadNib:(NSWindowController *)controller { + [super windowControllerDidLoadNib:controller]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(userDefaultsDidChange:) + name:NSUserDefaultsDidChangeNotification + object:nil]; + + _spellOutNumberFormatter = [[NSNumberFormatter alloc] init]; + + NSPressGestureRecognizer *pressGestureRecognizer = [[NSPressGestureRecognizer alloc] initWithTarget:self action:@selector(handlePressGesture:)]; + [self.mapView addGestureRecognizer:pressGestureRecognizer]; + + [self applyPendingState]; +} + +- (NSWindow *)window { + return self.windowControllers.firstObject.window; +} + +- (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 NSWindowDelegate methods + +- (void)window:(NSWindow *)window willEncodeRestorableState:(NSCoder *)state { + [state encodeObject:self.mapView.styleURL forKey:@"MBXMapViewStyleURL"]; +} + +- (void)window:(NSWindow *)window didDecodeRestorableState:(NSCoder *)state { + self.mapView.styleURL = [state decodeObjectForKey:@"MBXMapViewStyleURL"]; +} + +#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 streetsStyleURLWithVersion:MGLStyleDefaultVersion]; + break; + case 2: + styleURL = [MGLStyle outdoorsStyleURLWithVersion:MGLStyleDefaultVersion]; + break; + case 3: + styleURL = [MGLStyle lightStyleURLWithVersion:MGLStyleDefaultVersion]; + break; + case 4: + styleURL = [MGLStyle darkStyleURLWithVersion:MGLStyleDefaultVersion]; + break; + case 5: + styleURL = [MGLStyle satelliteStyleURLWithVersion:MGLStyleDefaultVersion]; + break; + case 6: + styleURL = [MGLStyle satelliteStreetsStyleURLWithVersion:MGLStyleDefaultVersion]; + 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, 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"]; + [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]; +} + +- (void)applyPendingState { + if (_inheritedStyleURL) { + self.mapView.styleURL = _inheritedStyleURL; + _inheritedStyleURL = nil; + } + + AppDelegate *appDelegate = (AppDelegate *)NSApp.delegate; + if (appDelegate.pendingStyleURL) { + self.mapView.styleURL = appDelegate.pendingStyleURL; + } + if (appDelegate.pendingCamera) { + if (appDelegate.pendingZoomLevel >= 0) { + self.mapView.zoomLevel = appDelegate.pendingZoomLevel; + appDelegate.pendingCamera.altitude = self.mapView.camera.altitude; + } + self.mapView.camera = appDelegate.pendingCamera; + appDelegate.pendingZoomLevel = -1; + appDelegate.pendingCamera = nil; + } + if (!MGLCoordinateBoundsIsEmpty(appDelegate.pendingVisibleCoordinateBounds)) { + self.mapView.visibleCoordinateBounds = appDelegate.pendingVisibleCoordinateBounds; + appDelegate.pendingVisibleCoordinateBounds = (MGLCoordinateBounds){ { 0, 0 }, { 0, 0 } }; + } + if (appDelegate.pendingDebugMask) { + self.mapView.debugMask = appDelegate.pendingDebugMask; + } + if (appDelegate.pendingMinimumZoomLevel >= 0) { + self.mapView.zoomLevel = MAX(appDelegate.pendingMinimumZoomLevel, self.mapView.zoomLevel); + appDelegate.pendingMaximumZoomLevel = -1; + } + if (appDelegate.pendingMaximumZoomLevel >= 0) { + self.mapView.zoomLevel = MIN(appDelegate.pendingMaximumZoomLevel, self.mapView.zoomLevel); + appDelegate.pendingMaximumZoomLevel = -1; + } + + // Temporarily set the display name to the default center coordinate instead + // of “Untitled” until the binding kicks in. + NSValue *coordinateValue = [NSValue valueWithMGLCoordinate:self.mapView.centerCoordinate]; + self.displayName = [[NSValueTransformer valueTransformerForName:@"LocationCoordinate2DTransformer"] + transformedValue:coordinateValue]; +} + +#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)toggleWireframes:(id)sender { + self.mapView.debugMask ^= MGLMapDebugWireframesMask; +} + +- (IBAction)showColorBuffer:(id)sender { + self.mapView.debugMask &= ~MGLMapDebugStencilBufferMask; +} + +- (IBAction)showStencilBuffer:(id)sender { + self.mapView.debugMask |= MGLMapDebugStencilBufferMask; +} + +- (IBAction)toggleShowsToolTipsOnDroppedPins:(id)sender { + _showsToolTipsOnDroppedPins = !_showsToolTipsOnDroppedPins; +} + +- (IBAction)toggleRandomizesCursorsOnDroppedPins:(id)sender { + _randomizesCursorsOnDroppedPins = !_randomizesCursorsOnDroppedPins; +} + +- (IBAction)dropManyPins:(id)sender { + [self removeAllAnnotations:sender]; + + 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)removeAllAnnotations:(id)sender { + [self.mapView removeAnnotations:self.mapView.annotations]; + _isShowingPolygonAndPolylineAnnotations = NO; +} + +- (IBAction)startWorldTour:(id)sender { + _isTouringWorld = YES; + + [self removeAllAnnotations: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; +} + +- (IBAction)drawPolygonAndPolyLineAnnotations:(id)sender { + + if (_isShowingPolygonAndPolylineAnnotations) { + [self removeAllAnnotations:sender]; + return; + } + + _isShowingPolygonAndPolylineAnnotations = YES; + + // Pacific Northwest triangle + CLLocationCoordinate2D triangleCoordinates[3] = { + CLLocationCoordinate2DMake(44, -122), + CLLocationCoordinate2DMake(46, -122), + CLLocationCoordinate2DMake(46, -121) + }; + MGLPolygon *triangle = [MGLPolygon polygonWithCoordinates:triangleCoordinates count:3]; + [self.mapView addAnnotation:triangle]; + + // West coast line + CLLocationCoordinate2D lineCoordinates[4] = { + CLLocationCoordinate2DMake(47.6025, -122.3327), + CLLocationCoordinate2DMake(45.5189, -122.6726), + CLLocationCoordinate2DMake(37.7790, -122.4177), + CLLocationCoordinate2DMake(34.0532, -118.2349) + }; + MGLPolyline *line = [MGLPolyline polylineWithCoordinates:lineCoordinates count:4]; + [self.mapView addAnnotation:line]; +} + +#pragma mark Offline packs + +- (IBAction)addOfflinePack:(id)sender { + NSAlert *namePrompt = [[NSAlert alloc] init]; + namePrompt.messageText = @"Add offline pack"; + namePrompt.informativeText = @"Choose a name for the pack:"; + NSTextField *nameTextField = [[NSTextField alloc] initWithFrame:NSZeroRect]; + nameTextField.placeholderString = MGLStringFromCoordinateBounds(self.mapView.visibleCoordinateBounds); + [nameTextField sizeToFit]; + NSRect textFieldFrame = nameTextField.frame; + textFieldFrame.size.width = 300; + nameTextField.frame = textFieldFrame; + namePrompt.accessoryView = nameTextField; + [namePrompt addButtonWithTitle:@"Add"]; + [namePrompt addButtonWithTitle:@"Cancel"]; + if ([namePrompt runModal] != NSAlertFirstButtonReturn) { + return; + } + + id <MGLOfflineRegion> region = [[MGLTilePyramidOfflineRegion alloc] initWithStyleURL:self.mapView.styleURL bounds:self.mapView.visibleCoordinateBounds fromZoomLevel:self.mapView.zoomLevel toZoomLevel:self.mapView.maximumZoomLevel]; + NSData *context = [[NSValueTransformer valueTransformerForName:@"OfflinePackNameValueTransformer"] reverseTransformedValue:nameTextField.stringValue]; + [[MGLOfflineStorage sharedOfflineStorage] addPackForRegion:region withContext:context completionHandler:^(MGLOfflinePack * _Nullable pack, NSError * _Nullable error) { + if (error) { + [[NSAlert alertWithError:error] runModal]; + } else { + [pack resume]; + } + }]; +} + +#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 + 1)]]; + [[NSWorkspace sharedWorkspace] openURL:feedbackURL]; +} + +#pragma mark Mouse events + +- (void)handlePressGesture:(NSPressGestureRecognizer *)gestureRecognizer { + if (gestureRecognizer.state == NSGestureRecognizerStateBegan) { + NSPoint location = [gestureRecognizer locationInView:self.mapView]; + if (!NSPointInRect([gestureRecognizer locationInView:self.mapView.compass], self.mapView.compass.bounds) + && !NSPointInRect([gestureRecognizer locationInView:self.mapView.zoomControls], self.mapView.zoomControls.bounds) + && !NSPointInRect([gestureRecognizer locationInView:self.mapView.attributionView], self.mapView.attributionView.bounds)) { + [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 { + NSArray *features = [self.mapView visibleFeaturesAtPoint:point]; + NSString *title; + for (id <MGLFeature> feature in features) { + if (!title) { + title = [feature attributeForKey:@"name_en"] ?: [feature attributeForKey:@"name"]; + } + } + + DroppedPinAnnotation *annotation = [[DroppedPinAnnotation alloc] init]; + annotation.coordinate = [self.mapView convertPoint:point toCoordinateFromView:self.mapView]; + annotation.title = 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]]; +} + +- (IBAction)selectFeatures:(id)sender { + [self selectFeaturesAtPoint:_mouseLocationForMapViewContextMenu]; +} + +- (void)selectFeaturesAtPoint:(NSPoint)point { + NSArray *features = [self.mapView visibleFeaturesAtPoint:point]; + NSArray *flattenedFeatures = MBXFlattenedShapes(features); + [self.mapView addAnnotations:flattenedFeatures]; +} + +#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 streetsStyleURLWithVersion:MGLStyleDefaultVersion]]; + break; + case 2: + state = [styleURL isEqual:[MGLStyle outdoorsStyleURLWithVersion:MGLStyleDefaultVersion]]; + break; + case 3: + state = [styleURL isEqual:[MGLStyle lightStyleURLWithVersion:MGLStyleDefaultVersion]]; + break; + case 4: + state = [styleURL isEqual:[MGLStyle darkStyleURLWithVersion:MGLStyleDefaultVersion]]; + break; + case 5: + state = [styleURL isEqual:[MGLStyle satelliteStyleURLWithVersion:MGLStyleDefaultVersion]]; + break; + case 6: + state = [styleURL isEqual:[MGLStyle satelliteStreetsStyleURLWithVersion:MGLStyleDefaultVersion]]; + 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:)) { + id <MGLAnnotation> annotationUnderCursor = [self.mapView annotationAtPoint:_mouseLocationForMapViewContextMenu]; + menuItem.hidden = annotationUnderCursor != nil; + return YES; + } + if (menuItem.action == @selector(removePin:)) { + id <MGLAnnotation> annotationUnderCursor = [self.mapView annotationAtPoint:_mouseLocationForMapViewContextMenu]; + menuItem.hidden = annotationUnderCursor == nil; + return YES; + } + if (menuItem.action == @selector(selectFeatures:)) { + 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(toggleWireframes:)) { + BOOL isShown = self.mapView.debugMask & MGLMapDebugWireframesMask; + menuItem.title = isShown ? @"Hide Wireframes" : @"Show Wireframes"; + return YES; + } + if (menuItem.action == @selector(showColorBuffer:)) { + BOOL enabled = self.mapView.debugMask & MGLMapDebugStencilBufferMask; + menuItem.state = enabled ? NSOffState : NSOnState; + return YES; + } + if (menuItem.action == @selector(showStencilBuffer:)) { + BOOL enabled = self.mapView.debugMask & MGLMapDebugStencilBufferMask; + menuItem.state = enabled ? NSOnState : NSOffState; + 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(removeAllAnnotations:)) { + return self.mapView.annotations.count > 0; + } + if (menuItem.action == @selector(startWorldTour:)) { + return !_isTouringWorld; + } + if (menuItem.action == @selector(stopWorldTour:)) { + return _isTouringWorld; + } + if (menuItem.action == @selector(drawPolygonAndPolyLineAnnotations:)) { + return !_isShowingPolygonAndPolylineAnnotations; + } + if (menuItem.action == @selector(addOfflinePack:)) { + NSURL *styleURL = self.mapView.styleURL; + return !styleURL.isFileURL; + } + if (menuItem.action == @selector(giveFeedback:)) { + return YES; + } + return NO; +} + +- (NSUInteger)indexOfStyleInToolbarItem { + if (![MGLAccountManager accessToken]) { + return NSNotFound; + } + + NSArray *styleURLs = @[ + [MGLStyle streetsStyleURLWithVersion:MGLStyleDefaultVersion], + [MGLStyle outdoorsStyleURLWithVersion:MGLStyleDefaultVersion], + [MGLStyle lightStyleURLWithVersion:MGLStyleDefaultVersion], + [MGLStyle darkStyleURLWithVersion:MGLStyleDefaultVersion], + [MGLStyle satelliteStyleURLWithVersion:MGLStyleDefaultVersion], + [MGLStyle satelliteStreetsStyleURLWithVersion:MGLStyleDefaultVersion], + ]; + 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 <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((uint32_t)cursors.count) % 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]; + } +} + +- (CGFloat)mapView:(MGLMapView *)mapView alphaForShapeAnnotation:(MGLShape *)annotation { + return 0.8; +} + +@end + +@interface ValidatedToolbarItem : NSToolbarItem + +@end + +@implementation ValidatedToolbarItem + +- (void)validate { + [(MapDocument *)self.toolbar.delegate validateToolbarItem:self]; +} + +@end diff --git a/platform/macos/app/OfflinePackNameValueTransformer.h b/platform/macos/app/OfflinePackNameValueTransformer.h new file mode 100644 index 0000000000..11fe3ff441 --- /dev/null +++ b/platform/macos/app/OfflinePackNameValueTransformer.h @@ -0,0 +1,5 @@ +#import <Foundation/Foundation.h> + +@interface OfflinePackNameValueTransformer : NSValueTransformer + +@end diff --git a/platform/macos/app/OfflinePackNameValueTransformer.m b/platform/macos/app/OfflinePackNameValueTransformer.m new file mode 100644 index 0000000000..2825e48ed3 --- /dev/null +++ b/platform/macos/app/OfflinePackNameValueTransformer.m @@ -0,0 +1,33 @@ +#import "OfflinePackNameValueTransformer.h" + +static NSString * const MBXOfflinePackContextNameKey = @"Name"; + +@implementation OfflinePackNameValueTransformer + ++ (Class)transformedValueClass { + return [NSString class]; +} + ++ (BOOL)allowsReverseTransformation { + return YES; +} + +- (NSString *)transformedValue:(NSData *)context { + NSAssert([context isKindOfClass:[NSData class]], @"Context should be NSData."); + + NSDictionary *userInfo = [NSKeyedUnarchiver unarchiveObjectWithData:context]; + NSAssert([userInfo isKindOfClass:[NSDictionary class]], @"Context of offline pack isn’t a dictionary."); + NSString *name = userInfo[MBXOfflinePackContextNameKey]; + NSAssert([name isKindOfClass:[NSString class]], @"Name of offline pack isn’t a string."); + return name; +} + +- (NSData *)reverseTransformedValue:(NSString *)name { + NSAssert([name isKindOfClass:[NSString class]], @"Name should be a string."); + + return [NSKeyedArchiver archivedDataWithRootObject:@{ + MBXOfflinePackContextNameKey: name, + }]; +} + +@end diff --git a/platform/macos/app/TimeIntervalTransformer.h b/platform/macos/app/TimeIntervalTransformer.h new file mode 100644 index 0000000000..ca88ad2cd1 --- /dev/null +++ b/platform/macos/app/TimeIntervalTransformer.h @@ -0,0 +1,5 @@ +#import <Foundation/Foundation.h> + +@interface TimeIntervalTransformer : NSValueTransformer + +@end diff --git a/platform/macos/app/TimeIntervalTransformer.m b/platform/macos/app/TimeIntervalTransformer.m new file mode 100644 index 0000000000..39177dc5bc --- /dev/null +++ b/platform/macos/app/TimeIntervalTransformer.m @@ -0,0 +1,53 @@ +#import "TimeIntervalTransformer.h" + +@implementation TimeIntervalTransformer + ++ (Class)transformedValueClass { + return [NSString class]; +} + ++ (BOOL)allowsReverseTransformation { + return NO; +} + +NSString *NumberAndUnitString(NSInteger quantity, NSString *singular, NSString *plural) { + return [NSString stringWithFormat:@"%ld %@", quantity, quantity == 1 ? singular : plural]; +} + +- (id)transformedValue:(id)value { + if (![value isKindOfClass:[NSValue class]]) { + return nil; + } + + NSTimeInterval timeInterval = [value doubleValue]; + NSInteger seconds = floor(timeInterval); + NSInteger minutes = floor(seconds / 60); + seconds -= minutes * 60; + NSInteger hours = floor(minutes / 60); + minutes -= hours * 60; + NSInteger days = floor(hours / 24); + hours -= days * 24; + NSInteger weeks = floor(days) / 7; + days -= weeks * 7; + + NSMutableArray *components = [NSMutableArray array]; + if (seconds || timeInterval < 60) { + [components addObject:NumberAndUnitString(seconds, @"second", @"seconds")]; + } + if (minutes) { + [components insertObject:NumberAndUnitString(minutes, @"minute", @"minutes") atIndex:0]; + } + if (hours) { + [components insertObject:NumberAndUnitString(hours, @"hour", @"hours") atIndex:0]; + } + if (days) { + [components insertObject:NumberAndUnitString(days, @"day", @"days") atIndex:0]; + } + if (weeks) { + [components insertObject:NumberAndUnitString(weeks, @"week", @"weeks") atIndex:0]; + } + + return [components componentsJoinedByString:@", "]; +} + +@end diff --git a/platform/macos/app/main.m b/platform/macos/app/main.m new file mode 100644 index 0000000000..8a6799b414 --- /dev/null +++ b/platform/macos/app/main.m @@ -0,0 +1,5 @@ +#import <Cocoa/Cocoa.h> + +int main(int argc, const char * argv[]) { + return NSApplicationMain(argc, argv); +} |