#import "AboutWindowController.h" #import #import #import #import #import #import static NSString* const kUserDefaultsReleaseNotesDigestKey = @"releaseNotesDigest"; static NSData* Digest (NSString* someString) { char const* str = [someString UTF8String]; char md[CC_SHA1_DIGEST_LENGTH]; CC_SHA1((unsigned char*)str, strlen(str), (unsigned char*)md); return [NSData dataWithBytes:md length:sizeof(md)]; } // ======================= // = Registration Window = // ======================= @interface RegistrationWindowController : NSWindowController @property (nonatomic) NSTextField* ownerLabel; @property (nonatomic) NSTextField* ownerTextField; @property (nonatomic) NSTextField* licenseLabel; @property (nonatomic) NSTextField* licenseTextField; @property (nonatomic) NSTextField* statusTextField; @property (nonatomic) NSButton* cancelButton; @property (nonatomic) NSButton* registerButton; @property (nonatomic) NSObjectController* objectController; @property (nonatomic) NSString* ownerString; @property (nonatomic) NSString* licenseString; @property (nonatomic) NSString* statusString; @property (nonatomic) BOOL canRegister; @end static NSTextField* OakCreateTextField () { NSTextField* res = [[NSTextField alloc] initWithFrame:NSZeroRect]; res.font = OakControlFont(); [[res cell] setWraps:YES]; return res; } #ifndef CONSTRAINT #define CONSTRAINT(str, align) [constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:str options:align metrics:nil views:views]] #endif @implementation RegistrationWindowController + (instancetype)sharedInstance { static RegistrationWindowController* sharedInstance = [RegistrationWindowController new]; return sharedInstance; } - (id)init { if((self = [super initWithWindow:[[NSPanel alloc] initWithContentRect:NSZeroRect styleMask:(NSTitledWindowMask|NSClosableWindowMask|NSMiniaturizableWindowMask) backing:NSBackingStoreBuffered defer:NO]])) { self.window.title = @"Add License"; self.window.delegate = self; self.ownerLabel = OakCreateLabel(@"Owner:"); self.ownerTextField = OakCreateTextField(); self.licenseLabel = OakCreateLabel(@"License:"); self.licenseTextField = OakCreateTextField(); self.licenseTextField.font = [NSFont userFixedPitchFontOfSize:12]; self.statusTextField = OakCreateSmallLabel(); self.cancelButton = OakCreateButton(@"Cancel"); self.registerButton = OakCreateButton(@"Register"); self.objectController = [[NSObjectController alloc] initWithContent:self]; [self.ownerTextField bind:NSValueBinding toObject:_objectController withKeyPath:@"content.ownerString" options:@{ NSContinuouslyUpdatesValueBindingOption: @YES }]; [self.licenseTextField bind:NSValueBinding toObject:_objectController withKeyPath:@"content.licenseString" options:@{ NSContinuouslyUpdatesValueBindingOption: @YES }]; [self.statusTextField bind:NSValueBinding toObject:_objectController withKeyPath:@"content.statusString" options:nil]; [self.registerButton bind:NSEnabledBinding toObject:_objectController withKeyPath:@"content.canRegister" options:nil]; self.registerButton.action = @selector(addLicense:); self.cancelButton.action = @selector(cancelOperation:); NSView* keyViewLoop[] = { self.ownerTextField, self.licenseTextField, self.cancelButton, self.registerButton }; for(size_t i = 0; i < sizeofA(keyViewLoop); ++i) keyViewLoop[i].nextKeyView = keyViewLoop[(i + 1) % sizeofA(keyViewLoop)]; self.window.initialFirstResponder = self.ownerTextField; self.window.defaultButtonCell = self.registerButton.cell; NSDictionary* views = @{ @"ownerLabel" : self.ownerLabel, @"owner" : self.ownerTextField, @"licenseLabel" : self.licenseLabel, @"license" : self.licenseTextField, @"status" : self.statusTextField, @"cancel" : self.cancelButton, @"register" : self.registerButton, }; NSView* contentView = self.window.contentView; for(NSView* view in [views allValues]) { [view setTranslatesAutoresizingMaskIntoConstraints:NO]; [contentView addSubview:view]; } NSMutableArray* constraints = [NSMutableArray array]; CONSTRAINT(@"H:|-[ownerLabel(==licenseLabel)]-[owner(==license)]-|", NSLayoutFormatAlignAllBaseline); CONSTRAINT(@"H:|-[licenseLabel]-[license(==400)]-|", 0); CONSTRAINT(@"H:[status(==license)]-|", 0); CONSTRAINT(@"H:[cancel]-[register]-|", NSLayoutFormatAlignAllTop); CONSTRAINT(@"V:|-[owner]-[license(==98)]-[status]-[register]-|", 0); [constraints addObject:[NSLayoutConstraint constraintWithItem:self.licenseLabel attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.licenseTextField attribute:NSLayoutAttributeTop multiplier:1 constant:3]]; [contentView addConstraints:constraints]; self.ownerString = NSFullUserName(); } return self; } - (IBAction)showWindow:(id)sender { if(![self.window isVisible]) { [self.window layoutIfNeeded]; [self.window center]; } [self.window makeKeyAndOrderFront:self]; } - (NSString*)trimmedOwnerString { return [self.ownerString stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; } - (void)validateOwnerAndLicense { bool hasContent = NSNotEmptyString(self.trimmedOwnerString) && NSNotEmptyString(self.licenseString); bool validLicense = hasContent && license::is_valid(license::decode(to_s(self.licenseString)), to_s(self.trimmedOwnerString)); self.canRegister = validLicense; self.statusString = validLicense || !hasContent ? nil : [NSString stringWithCxxString:license::error_description(to_s(self.licenseString), to_s(self.trimmedOwnerString))]; if(validLicense) { auto const license = license::decode(to_s(self.licenseString)); dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ if(license::is_revoked(license)) { dispatch_async(dispatch_get_main_queue(), ^{ self.canRegister = NO; self.statusString = @"This license has been revoked."; }); } }); } } - (void)setOwnerString:(NSString*)aString { if(_ownerString != aString && ![_ownerString isEqualToString:aString]) { _ownerString = aString; [self validateOwnerAndLicense]; } } - (void)setLicenseString:(NSString*)aString { if(_licenseString != aString && ![_licenseString isEqualToString:aString]) { _licenseString = aString; [self validateOwnerAndLicense]; } } - (void)addLicense:(id)sender { [self close]; if(self.canRegister) { auto const license = license::decode(to_s(self.licenseString)); dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ bool revoked = license::is_revoked(license); dispatch_async(dispatch_get_main_queue(), ^{ std::string error = "Unknown error."; if(revoked) NSRunAlertPanel(@"License Has Been Revoked", @"The license provided is no longer valid.\n\nThe most likely reason for revocation is that a chargeback was issued for your credit card transaction.", @"Continue", nil, nil); else if(license::add(to_s(self.trimmedOwnerString), to_s(self.licenseString), &error)) NSRunAlertPanel(@"License Added to Keychain", @"Thanks for your support!", @"Continue", nil, nil); else NSRunAlertPanel(@"Failure Adding License to Keychain", [NSString stringWithCxxString:error], @"Continue", nil, nil, getprogname()); }); }); } } @end // ============================ // = JavaScript Bridge Object = // ============================ @interface AboutWindowJSBridge : NSObject { NSString* version; NSString* licensees; } - (void)addLicense; @end @implementation AboutWindowJSBridge + (BOOL)isSelectorExcludedFromWebScript:(SEL)aSelector { return aSelector != @selector(addLicense); } + (BOOL)isKeyExcludedFromWebScript:(char const*)name { return strcmp(name, "version") != 0 && strcmp(name, "licensees") != 0; } + (NSString*)webScriptNameForSelector:(SEL)aSelector { return NSStringFromSelector(aSelector); } + (NSString*)webScriptNameForKey:(char const*)name { return @(name); } - (NSString*)version { return [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"]; } - (NSString*)licensees { if(!licensees) { for(auto owner : license::find_all()) { if(license::is_valid(license::decode(license::find(owner)), owner)) { licensees = [NSString stringWithCxxString:owner]; break; } } } return licensees; } - (void)addLicense { [[RegistrationWindowController sharedInstance] showWindow:self]; } @end // ============================ @interface AboutWindowController () @property (nonatomic) NSToolbar* toolbar; @property (nonatomic) WebView* webView; @property (nonatomic) NSString* selectedPage; @end @implementation AboutWindowController + (AboutWindowController*)sharedInstance { static AboutWindowController* instance = [AboutWindowController new]; return instance; } + (void)showChangesIfUpdated { NSURL* url = [[NSBundle mainBundle] URLForResource:@"Changes" withExtension:@"html"]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{ if(NSString* releaseNotes = [NSString stringWithContentsOfURL:url encoding:NSUTF8StringEncoding error:NULL]) { NSData* lastDigest = [[NSUserDefaults standardUserDefaults] dataForKey:kUserDefaultsReleaseNotesDigestKey]; NSData* currentDigest = Digest(releaseNotes); if(lastDigest && ![lastDigest isEqualToData:currentDigest]) { dispatch_async(dispatch_get_main_queue(), ^{ [[AboutWindowController sharedInstance] showChangesWindow:self]; }); } [[NSUserDefaults standardUserDefaults] setObject:currentDigest forKey:kUserDefaultsReleaseNotesDigestKey]; } }); } - (id)init { NSRect visibleRect = [[NSScreen mainScreen] visibleFrame]; NSRect rect = NSMakeRect(0, 0, std::min(700, NSWidth(visibleRect)), std::min(800, NSHeight(visibleRect))); CGFloat dy = NSHeight(visibleRect) - NSHeight(rect); rect.origin.y = round(NSMinY(visibleRect) + dy*3/4); rect.origin.x = NSMaxY(visibleRect) - NSMaxY(rect); NSWindow* win = [[NSWindow alloc] initWithContentRect:rect styleMask:(NSTitledWindowMask|NSClosableWindowMask|NSResizableWindowMask|NSMiniaturizableWindowMask) backing:NSBackingStoreBuffered defer:NO]; if((self = [super initWithWindow:win])) { self.toolbar = [[NSToolbar alloc] initWithIdentifier:@"About TextMate"]; [self.toolbar setAllowsUserCustomization:NO]; [self.toolbar setDisplayMode:NSToolbarDisplayModeLabelOnly]; [self.toolbar setDelegate:self]; [win setToolbar:self.toolbar]; NSView* contentView = [[NSView alloc] initWithFrame:NSZeroRect]; [win setTitle:@"About TextMate"]; [win setContentView:contentView]; [win setFrameAutosaveName:@"BundlesReleaseNotes"]; [win setDelegate:self]; [win setAutorecalculatesKeyViewLoop:YES]; [win setReleasedWhenClosed:NO]; self.webView = [[WebView alloc] initWithFrame:[contentView bounds]]; self.webView.translatesAutoresizingMaskIntoConstraints = NO; self.webView.frameLoadDelegate = self; self.webView.policyDelegate = self; [contentView addSubview:self.webView]; NSDictionary* views = @{ @"webView" : self.webView }; [contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[webView(>=200)]|" options:NSLayoutFormatAlignAllTop metrics:nil views:views]]; [contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[webView(>=200)]|" options:NSLayoutFormatAlignAllLeading metrics:nil views:views]]; } return self; } - (void)dealloc { [self.webView setFrameLoadDelegate:nil]; [[self.webView mainFrame] stopLoading]; } - (void)showAboutWindow:(id)sender { self.selectedPage = @"About"; [self showWindow:self]; } - (void)showChangesWindow:(id)sender { self.selectedPage = @"Changes"; [self showWindow:self]; NSURL* url = [[NSBundle mainBundle] URLForResource:@"Changes" withExtension:@"html"]; if(NSString* releaseNotes = [NSString stringWithContentsOfURL:url encoding:NSUTF8StringEncoding error:NULL]) [[NSUserDefaults standardUserDefaults] setObject:Digest(releaseNotes) forKey:kUserDefaultsReleaseNotesDigestKey]; } - (void)setSelectedPage:(NSString*)pageName { if(_selectedPage == pageName || [_selectedPage isEqualToString:pageName]) return; _selectedPage = pageName; NSDictionary* pages = @{ @"About" : @"About", @"Changes" : @"Changes", @"Bundles" : @"Bundles", @"Registration" : @"Registration", @"Legal" : @"Legal", @"Contributions" : @"Contributions" }; if(NSString* file = pages[pageName]) { if(NSURL* url = [[NSBundle mainBundle] URLForResource:file withExtension:@"html"]) [[self.webView mainFrame] loadRequest:[NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:60]]; [self.window setTitle:pageName]; [self.toolbar setSelectedItemIdentifier:pageName]; } } - (void)selectPageAtRelativeOffset:(NSInteger)offset { NSArray* allPages = [self toolbarSelectableItemIdentifiers:nil]; NSUInteger index = [allPages indexOfObject:self.selectedPage]; if(index != NSNotFound) self.selectedPage = allPages[(index + allPages.count + offset) % allPages.count]; } - (IBAction)selectNextTab:(id)sender { [self selectPageAtRelativeOffset:+1]; } - (IBAction)selectPreviousTab:(id)sender { [self selectPageAtRelativeOffset:-1]; } // ==================== // = Toolbar Delegate = // ==================== - (void)didClickToolbarItem:(id)sender { NSString* identifier = nil; if([sender respondsToSelector:@selector(itemIdentifier)]) identifier = [sender itemIdentifier]; else if([sender respondsToSelector:@selector(representedObject)]) identifier = [sender representedObject]; if(identifier) self.selectedPage = identifier; } - (NSToolbarItem*)toolbar:(NSToolbar*)aToolbar itemForItemIdentifier:(NSString*)anIdentifier willBeInsertedIntoToolbar:(BOOL)flag { NSToolbarItem* res = [[NSToolbarItem alloc] initWithItemIdentifier:anIdentifier]; [res setLabel:anIdentifier]; [res setTarget:self]; [res setAction:@selector(didClickToolbarItem:)]; return res; } - (NSArray*)toolbarAllowedItemIdentifiers:(NSToolbar*)aToolbar { return @[ @"About", @"Changes", @"Bundles", NSToolbarFlexibleSpaceItemIdentifier, @"Registration", @"Legal", @"Contributions" ]; } - (NSArray*)toolbarDefaultItemIdentifiers:(NSToolbar*)aToolbar { return [self toolbarAllowedItemIdentifiers:aToolbar]; } - (NSArray*)toolbarSelectableItemIdentifiers:(NSToolbar*)aToolbar { return @[ @"About", @"Changes", @"Bundles", @"Registration", @"Legal", @"Contributions" ]; } // ==================== - (void)updateGoToMenu:(NSMenu*)aMenu { if(![[self window] isKeyWindow]) { [aMenu addItemWithTitle:@"No Tabs" action:@selector(nop:) keyEquivalent:@""]; return; } char key = '0'; for(NSString* label in [self toolbarSelectableItemIdentifiers:self.toolbar]) { NSMenuItem* item = [aMenu addItemWithTitle:label action:@selector(didClickToolbarItem:) keyEquivalent:key++ < '9' ? [NSString stringWithFormat:@"%c", key] : @""]; [item setRepresentedObject:label]; [item setTarget:self]; [item setState:[label isEqualToString:[self.toolbar selectedItemIdentifier]] ? NSOnState : NSOffState]; } } // ==================== static NSDictionary* RemoveOldCommits (NSDictionary* src) { NSMutableDictionary* res = [src mutableCopy]; NSMutableArray* commits = [NSMutableArray array]; for(NSDictionary* commit in src[@"commits"]) { NSString* dateString = commit[@"date"]; for(NSString* prefix in @[ @"2012-", @"2013-" ]) { if([dateString hasPrefix:prefix]) // this is significantly faster than having to parse the date [commits addObject:commit]; } } res[@"commits"] = commits; return res; } - (void)webView:(WebView*)aWebView didFinishLoadForFrame:(WebFrame*)aFrame { if(![[self.toolbar selectedItemIdentifier] isEqualToString:@"Bundles"]) return; bool first = true; NSMutableString* str = [NSMutableString stringWithString:@"{\"bundles\":["]; for(std::string path : bundles_db::release_notes()) { NSError* err = NULL; if(NSString* content = [NSString stringWithContentsOfFile:[NSString stringWithCxxString:path] encoding:NSUTF8StringEncoding error:&err]) { if(NSDictionary* obj = [NSJSONSerialization JSONObjectWithData:[content dataUsingEncoding:NSUTF8StringEncoding] options:0 error:&err]) { if(NSData* data = [NSJSONSerialization dataWithJSONObject:RemoveOldCommits(obj) options:0 error:&err]) { if(!first) [str appendString:@","]; first = false; [str appendString:[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]]; continue; } } } NSLog(@"%s: %@", path.c_str(), err.localizedDescription); } [str appendString:@"]}"]; WebScriptObject* scriptObject = [aWebView windowScriptObject]; [scriptObject callWebScriptMethod:@"setJSON" withArguments:@[ str ]]; } - (void)webView:(WebView*)sender didClearWindowObject:(WebScriptObject*)windowScriptObject forFrame:(WebFrame*)frame { AboutWindowJSBridge* bridge = [[AboutWindowJSBridge alloc] init]; [windowScriptObject setValue:bridge forKey:@"TextMate"]; } - (void)webView:(WebView*)sender decidePolicyForNavigationAction:(NSDictionary*)actionInformation request:(NSURLRequest*)request frame:(WebFrame*)frame decisionListener:(id )listener { if(![[request.URL scheme] isEqualToString:@"file"] && [[NSWorkspace sharedWorkspace] openURL:request.URL]) [listener ignore]; else if([NSURLConnection canHandleRequest:request]) [listener use]; } @end