X-Git-Url: https://git.yukkurigames.com/?p=enjoyable.git;a=blobdiff_plain;f=Classes%2FNJMappingsController.m;h=4859970fc63ff07b81788bf655978ec33d63f019;hp=2ee423905cde287893e165ada1e395d00ce25312;hb=a3d6f991d110dcccb70e137f43cbafc60f7ecee7;hpb=7e5568674713bedf9318e83b9fb13abbd122382c diff --git a/Classes/NJMappingsController.m b/Classes/NJMappingsController.m index 2ee4239..4859970 100644 --- a/Classes/NJMappingsController.m +++ b/Classes/NJMappingsController.m @@ -10,32 +10,26 @@ #import "NJMapping.h" #import "NJMappingsController.h" #import "NJOutput.h" -#import "NJOutputController.h" #import "NJEvents.h" #define PB_ROW @"com.yukkurigames.Enjoyable.MappingRow" @implementation NJMappingsController { NSMutableArray *_mappings; - NJMapping *manualMapping; - NSString *draggingName; + NJMapping *_manualMapping; } - (id)init { if ((self = [super init])) { _mappings = [[NSMutableArray alloc] init]; - _currentMapping = [[NJMapping alloc] initWithName:@"(default)"]; - manualMapping = _currentMapping; + _currentMapping = [[NJMapping alloc] initWithName: + NSLocalizedString(@"(default)", @"default name for first the mapping")]; + _manualMapping = _currentMapping; [_mappings addObject:_currentMapping]; } return self; } -- (void)awakeFromNib { - [tableView registerForDraggedTypes:@[PB_ROW, NSURLPboardType]]; - [tableView setDraggingSourceOperationMask:NSDragOperationCopy forLocal:NO]; -} - - (NJMapping *)objectForKeyedSubscript:(NSString *)name { for (NJMapping *mapping in _mappings) if ([name isEqualToString:mapping.name]) @@ -47,14 +41,18 @@ return idx < _mappings.count ? _mappings[idx] : nil; } -- (void)mappingsChanged { - [self save]; - [tableView reloadData]; - popoverActivate.title = _currentMapping.name; - [self updateInterfaceForCurrentMapping]; +- (void)mappingsSet { [NSNotificationCenter.defaultCenter postNotificationName:NJEventMappingListChanged - object:_mappings]; + object:self + userInfo:@{ NJMappingListKey: _mappings, + NJMappingKey: _currentMapping }]; + [self.mvc changedActiveMappingToIndex:[_mappings indexOfObjectIdenticalTo:_currentMapping]]; +} + +- (void)mappingsChanged { + [self save]; + [self mappingsSet]; } - (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state @@ -66,13 +64,13 @@ } - (void)activateMappingForProcess:(NSRunningApplication *)app { - NJMapping *oldMapping = manualMapping; + NJMapping *oldMapping = _manualMapping; NSArray *names = app.possibleMappingNames; BOOL found = NO; for (NSString *name in names) { NJMapping *mapping = self[name]; if (mapping) { - [self activateMapping:self[name]]; + [self activateMapping:mapping]; found = YES; break; } @@ -80,78 +78,29 @@ if (!found) { [self activateMapping:oldMapping]; - if ([oldMapping.name.lowercaseString isEqualToString:@"@application"]) { + if ([oldMapping.name.lowercaseString isEqualToString:@"@application"] + || [oldMapping.name.lowercaseString isEqualToString: + NSLocalizedString(@"@Application", nil).lowercaseString]) { oldMapping.name = app.bestMappingName; [self mappingsChanged]; } } - manualMapping = oldMapping; -} - -- (void)updateInterfaceForCurrentMapping { - NSUInteger selected = [_mappings indexOfObject:_currentMapping]; - [removeButton setEnabled:selected != 0]; - [moveDown setEnabled:selected && selected != _mappings.count - 1]; - [moveUp setEnabled:selected > 1]; - popoverActivate.title = _currentMapping.name; - [tableView selectRowIndexes:[NSIndexSet indexSetWithIndex:selected] byExtendingSelection:NO]; - [NSUserDefaults.standardUserDefaults setInteger:selected forKey:@"selected"]; + _manualMapping = oldMapping; } - (void)activateMapping:(NJMapping *)mapping { if (!mapping) - mapping = manualMapping; + mapping = _manualMapping; if (mapping == _currentMapping) return; NSLog(@"Switching to mapping %@.", mapping.name); - manualMapping = mapping; + _manualMapping = mapping; _currentMapping = mapping; - [self updateInterfaceForCurrentMapping]; - [outputController loadCurrent]; - [NSNotificationCenter.defaultCenter postNotificationName:NJEventMappingChanged - object:_currentMapping]; -} - -- (IBAction)addPressed:(id)sender { - NJMapping *newMapping = [[NJMapping alloc] initWithName:@"Untitled"]; - [_mappings addObject:newMapping]; - [self activateMapping:newMapping]; - [self mappingsChanged]; - [tableView editColumn:0 row:_mappings.count - 1 withEvent:nil select:YES]; -} - -- (IBAction)removePressed:(id)sender { - if (tableView.selectedRow == 0) - return; - - NSInteger selectedRow = tableView.selectedRow; - [_mappings removeObjectAtIndex:selectedRow]; - [self activateMapping:_mappings[MIN(selectedRow, _mappings.count - 1)]]; - [self mappingsChanged]; -} - -- (void)tableViewSelectionDidChange:(NSNotification *)notify { - [self activateMapping:self[tableView.selectedRow]]; -} - -- (id)tableView:(NSTableView *)view objectValueForTableColumn:(NSTableColumn *)column row:(NSInteger)index { - return self[index].name; -} - -- (void)tableView:(NSTableView *)view - setObjectValue:(NSString *)obj - forTableColumn:(NSTableColumn *)col - row:(NSInteger)index { - self[index].name = obj; - [self mappingsChanged]; -} - -- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView { - return _mappings.count; -} - -- (BOOL)tableView:(NSTableView *)view shouldEditTableColumn:(NSTableColumn *)column row:(NSInteger)index { - return YES; + [self.mvc changedActiveMappingToIndex:[_mappings indexOfObjectIdenticalTo:_currentMapping]]; + [NSNotificationCenter.defaultCenter + postNotificationName:NJEventMappingChanged + object:self + userInfo:@{ NJMappingKey : _currentMapping }]; } - (void)save { @@ -164,281 +113,168 @@ - (void)load { NSUInteger selected = [NSUserDefaults.standardUserDefaults integerForKey:@"selected"]; - NSArray *mappings = [NSUserDefaults.standardUserDefaults arrayForKey:@"mappings"]; - [self loadAllFrom:mappings andActivate:selected]; -} - -- (void)loadAllFrom:(NSArray *)storedMappings andActivate:(NSUInteger)selected { + NSArray *storedMappings = [NSUserDefaults.standardUserDefaults arrayForKey:@"mappings"]; NSMutableArray* newMappings = [[NSMutableArray alloc] initWithCapacity:storedMappings.count]; - // have to do two passes in case mapping1 refers to mapping2 via a NJOutputMapping + // Requires two passes to deal with inter-mapping references. First make + // an empty mapping for each serialized mapping. Then, deserialize the + // data pointing to the empty mappings. Then merge that data back into + // its equivalent empty one, which is the one we finally use. for (NSDictionary *storedMapping in storedMappings) { NJMapping *mapping = [[NJMapping alloc] initWithName:storedMapping[@"name"]]; [newMappings addObject:mapping]; } for (unsigned i = 0; i < storedMappings.count; ++i) { - NSDictionary *entries = storedMappings[i][@"entries"]; - NJMapping *mapping = newMappings[i]; - for (id key in entries) { - NJOutput *output = [NJOutput outputDeserialize:entries[key] - withMappings:newMappings]; - if (output) - mapping.entries[key] = output; - } + NJMapping *realMapping = [[NJMapping alloc] initWithSerialization:storedMappings[i] + mappings:newMappings]; + [newMappings[i] mergeEntriesFrom:realMapping]; } if (newMappings.count) { _mappings = newMappings; if (selected >= newMappings.count) selected = 0; + [self.mvc reloadData]; [self activateMapping:_mappings[selected]]; - [self mappingsChanged]; + [self mappingsSet]; } } -- (NJMapping *)mappingWithURL:(NSURL *)url error:(NSError **)error { - NSInputStream *stream = [NSInputStream inputStreamWithURL:url]; - [stream open]; - NSDictionary *serialization = !*error - ? [NSJSONSerialization JSONObjectWithStream:stream options:0 error:error] - : nil; - [stream close]; - - if (!([serialization isKindOfClass:NSDictionary.class] - && [serialization[@"name"] isKindOfClass:NSString.class] - && [serialization[@"entries"] isKindOfClass:NSDictionary.class])) { - *error = [NSError errorWithDomain:@"Enjoyable" - code:0 - description:@"This isn't a valid mapping file."]; - return nil; +- (void)mappingConflictDidResolve:(NSAlert *)alert + returnCode:(NSInteger)returnCode + contextInfo:(void *)contextInfo { + NSDictionary *userInfo = CFBridgingRelease(contextInfo); + NJMapping *oldMapping = userInfo[@"old mapping"]; + NJMapping *newMapping = userInfo[@"new mapping"]; + [alert.window orderOut:nil]; + switch (returnCode) { + case NSAlertFirstButtonReturn: // Merge + [oldMapping mergeEntriesFrom:newMapping]; + _currentMapping = nil; + [self activateMapping:oldMapping]; + [self mappingsChanged]; + break; + case NSAlertThirdButtonReturn: // New Mapping + [self.mvc.mappingList beginUpdates]; + [_mappings addObject:newMapping]; + [self.mvc addedMappingAtIndex:_mappings.count - 1 startEditing:YES]; + [self.mvc.mappingList endUpdates]; + [self activateMapping:newMapping]; + [self mappingsChanged]; + break; + default: // Cancel, other. + break; } +} - NSDictionary *entries = serialization[@"entries"]; - NJMapping *mapping = [[NJMapping alloc] initWithName:serialization[@"name"]]; - for (id key in entries) { - NSDictionary *value = entries[key]; - if ([key isKindOfClass:NSString.class]) { - NJOutput *output = [NJOutput outputDeserialize:value - withMappings:_mappings]; - if (output) - mapping.entries[key] = output; - } - } - return mapping; +- (void)addOrMergeMapping:(NJMapping *)mapping { + [self addOrMergeMapping:mapping atIndex:-1]; } -- (void)addMappingWithContentsOfURL:(NSURL *)url { - NSWindow *window = popoverActivate.window; - NSError *error; - NJMapping *mapping = [NJMapping mappingWithContentsOfURL:url - mappings:_mappings - error:&error]; - - if (mapping && !error) { - BOOL conflict = NO; +- (void)addOrMergeMapping:(NJMapping *)mapping atIndex:(NSInteger)idx { + NSWindow *window = NSApplication.sharedApplication.keyWindow; + if (mapping) { NJMapping *mergeInto = self[mapping.name]; - for (id key in mapping.entries) { - if (mergeInto.entries[key] - && ![mergeInto.entries[key] isEqual:mapping.entries[key]]) { - conflict = YES; - break; - } - } - - if (conflict) { + if ([mergeInto hasConflictWith:mapping]) { NSAlert *conflictAlert = [[NSAlert alloc] init]; - conflictAlert.messageText = @"Replace existing mappings?"; + conflictAlert.messageText = NSLocalizedString(@"import conflict prompt", @"Title of import conflict alert"); conflictAlert.informativeText = - [NSString stringWithFormat: - @"This file contains inputs you've already mapped in \"%@\". Do you " - @"want to merge them and replace your existing mappings, or import this " - @"as a separate mapping?", mapping.name]; - [conflictAlert addButtonWithTitle:@"Merge"]; - [conflictAlert addButtonWithTitle:@"Cancel"]; - [conflictAlert addButtonWithTitle:@"New Mapping"]; - NSInteger res = [conflictAlert runModal]; - if (res == NSAlertSecondButtonReturn) - return; - else if (res == NSAlertThirdButtonReturn) - mergeInto = nil; - } - - if (mergeInto) { - [mergeInto.entries addEntriesFromDictionary:mapping.entries]; - mapping = mergeInto; + [NSString stringWithFormat:NSLocalizedString(@"import conflict in %@", @"Explanation of import conflict"), + mapping.name]; + [conflictAlert addButtonWithTitle:NSLocalizedString(@"import and merge", @"button to merge imported mappings")]; + [conflictAlert addButtonWithTitle:NSLocalizedString(@"cancel import", @"button to cancel import")]; + [conflictAlert addButtonWithTitle:NSLocalizedString(@"import new mapping", @"button to import as new mapping")]; + [conflictAlert beginSheetModalForWindow:window + modalDelegate:self + didEndSelector:@selector(mappingConflictDidResolve:returnCode:contextInfo:) + contextInfo:(void *)CFBridgingRetain(@{ @"old mapping": mergeInto, + @"new mapping": mapping })]; + } else if (mergeInto) { + [mergeInto mergeEntriesFrom:mapping]; + [self activateMapping:mergeInto]; + [self mappingsChanged]; } else { - [_mappings addObject:mapping]; - } - - [self activateMapping:mapping]; - [self mappingsChanged]; - - if (conflict && !mergeInto) { - [tableView selectRowIndexes:[NSIndexSet indexSetWithIndex:_mappings.count - 1] byExtendingSelection:NO]; - [tableView editColumn:0 row:_mappings.count - 1 withEvent:nil select:YES]; + if (idx == -1) + idx = _mappings.count; + [self.mvc.mappingList beginUpdates]; + [_mappings insertObject:mapping atIndex:idx]; + [self.mvc addedMappingAtIndex:idx startEditing:NO]; + [self.mvc.mappingList endUpdates]; + [self activateMapping:mapping]; + [self mappingsChanged]; } } - - if (error) { - [window presentError:error - modalForWindow:window - delegate:nil - didPresentSelector:nil - contextInfo:nil]; - } -} - -- (void)importPressed:(id)sender { - NSOpenPanel *panel = [NSOpenPanel openPanel]; - panel.allowedFileTypes = @[ @"enjoyable", @"json", @"txt" ]; - NSWindow *window = NSApplication.sharedApplication.keyWindow; - [panel beginSheetModalForWindow:window - completionHandler:^(NSInteger result) { - if (result != NSFileHandlingPanelOKButton) - return; - [panel close]; - [self addMappingWithContentsOfURL:panel.URL]; - }]; - } -- (void)exportPressed:(id)sender { - NSSavePanel *panel = [NSSavePanel savePanel]; - panel.allowedFileTypes = @[ @"enjoyable" ]; - NJMapping *mapping = _currentMapping; - panel.nameFieldStringValue = [mapping.name stringByFixingPathComponent]; - NSWindow *window = NSApplication.sharedApplication.keyWindow; - [panel beginSheetModalForWindow:window - completionHandler:^(NSInteger result) { - if (result != NSFileHandlingPanelOKButton) - return; - [panel close]; - NSError *error; - [mapping writeToURL:panel.URL error:&error]; - if (error) { - [window presentError:error - modalForWindow:window - delegate:nil - didPresentSelector:nil - contextInfo:nil]; - } - }]; -} - -- (IBAction)mappingPressed:(id)sender { - [popover showRelativeToRect:popoverActivate.bounds - ofView:popoverActivate - preferredEdge:NSMinXEdge]; +- (NSInteger)numberOfMappings:(NJMappingsViewController *)dvc { + return _mappings.count; } -- (void)popoverWillShow:(NSNotification *)notification { - popoverActivate.state = NSOnState; +- (NJMapping *)mappingsViewController:(NJMappingsViewController *)dvc + mappingForIndex:(NSUInteger)idx { + return _mappings[idx]; } -- (void)popoverWillClose:(NSNotification *)notification { - popoverActivate.state = NSOffState; +- (void)mappingsViewController:(NJMappingsViewController *)mvc + editedMappingAtIndex:(NSInteger)index { + [self mappingsChanged]; } -- (IBAction)moveUpPressed:(id)sender { - NSUInteger idx = [_mappings indexOfObject:_currentMapping]; - if (idx > 1 && idx != NSNotFound) { - [_mappings exchangeObjectAtIndex:idx withObjectAtIndex:idx - 1]; - [self mappingsChanged]; - } +- (BOOL)mappingsViewController:(NJMappingsViewController *)mvc + canMoveMappingFromIndex:(NSInteger)fromIdx + toIndex:(NSInteger)toIdx { + return fromIdx != toIdx && fromIdx != 0 && toIdx != 0 && toIdx < (NSInteger)_mappings.count; } -- (IBAction)moveDownPressed:(id)sender { - NSUInteger idx = [_mappings indexOfObject:_currentMapping]; - if (idx < _mappings.count - 1) { - [_mappings exchangeObjectAtIndex:idx withObjectAtIndex:idx + 1]; - [self mappingsChanged]; - } +- (void)mappingsViewController:(NJMappingsViewController *)mvc + moveMappingFromIndex:(NSInteger)fromIdx + toIndex:(NSInteger)toIdx { + [_mappings moveObjectAtIndex:fromIdx toIndex:toIdx]; + [self mappingsChanged]; } -- (BOOL)tableView:(NSTableView *)tableView_ - acceptDrop:(id )info - row:(NSInteger)row - dropOperation:(NSTableViewDropOperation)dropOperation { - NSPasteboard *pboard = [info draggingPasteboard]; - if ([pboard.types containsObject:PB_ROW]) { - NSString *value = [pboard stringForType:PB_ROW]; - NSUInteger srcRow = [value intValue]; - [_mappings moveObjectAtIndex:srcRow toIndex:row]; - [self mappingsChanged]; - return YES; - } else if ([pboard.types containsObject:NSURLPboardType]) { - NSURL *url = [NSURL URLFromPasteboard:pboard]; - NSError *error; - NJMapping *mapping = [NJMapping mappingWithContentsOfURL:url - mappings:_mappings - error:&error]; - if (error) { - [tableView_ presentError:error]; - return NO; - } else { - [_mappings insertObject:mapping atIndex:row]; - [self mappingsChanged]; - return YES; - } - } else { - return NO; - } +- (BOOL)mappingsViewController:(NJMappingsViewController *)mvc + canRemoveMappingAtIndex:(NSInteger)idx { + return idx != 0; } -- (NSDragOperation)tableView:(NSTableView *)tableView_ - validateDrop:(id )info - proposedRow:(NSInteger)row - proposedDropOperation:(NSTableViewDropOperation)dropOperation { - NSPasteboard *pboard = [info draggingPasteboard]; - if ([pboard.types containsObject:PB_ROW]) { - [tableView_ setDropRow:MAX(1, row) dropOperation:NSTableViewDropAbove]; - return NSDragOperationMove; - } else if ([pboard.types containsObject:NSURLPboardType]) { - NSURL *url = [NSURL URLFromPasteboard:pboard]; - if ([url.pathExtension isEqualToString:@"enjoyable"]) { - [tableView_ setDropRow:MAX(1, row) dropOperation:NSTableViewDropAbove]; - return NSDragOperationCopy; - } else { - return NSDragOperationNone; - } - } else { - return NSDragOperationNone; - } +- (void)mappingsViewController:(NJMappingsViewController *)mvc + removeMappingAtIndex:(NSInteger)idx { + NJMapping *old = self[idx]; + [self.mvc.mappingList beginUpdates]; + [_mappings removeObjectAtIndex:idx]; + [self.mvc removedMappingAtIndex:idx]; + [self.mvc.mappingList endUpdates]; + if (old == _currentMapping) + [self activateMapping:self[MIN(idx, _mappings.count - 1)]]; + [self mappingsChanged]; } -- (NSArray *)tableView:(NSTableView *)tableView_ -namesOfPromisedFilesDroppedAtDestination:(NSURL *)dropDestination -forDraggedRowsWithIndexes:(NSIndexSet *)indexSet { - NJMapping *toSave = self[indexSet.firstIndex]; - NSString *filename = [[toSave.name stringByFixingPathComponent] - stringByAppendingPathExtension:@"enjoyable"]; - NSURL *dst = [dropDestination URLByAppendingPathComponent:filename]; - dst = [NSFileManager.defaultManager generateUniqueURLWithBase:dst]; - NSError *error; - if (![toSave writeToURL:dst error:&error]) { - [tableView_ presentError:error]; - return @[]; - } else { - return @[dst.lastPathComponent]; - } +- (BOOL)mappingsViewController:(NJMappingsViewController *)mvc + importMappingFromURL:(NSURL *)url + atIndex:(NSInteger)index + error:(NSError **)error { + NJMapping *mapping = [NJMapping mappingWithContentsOfURL:url + mappings:_mappings + error:error]; + [self addOrMergeMapping:mapping atIndex:index]; + return !!mapping; +} + +- (void)mappingsViewController:(NJMappingsViewController *)mvc + addMapping:(NJMapping *)mapping { + [self.mvc.mappingList beginUpdates]; + [_mappings addObject:mapping]; + [self.mvc addedMappingAtIndex:_mappings.count - 1 startEditing:YES]; + [self.mvc.mappingList endUpdates]; + [self activateMapping:mapping]; + [self mappingsChanged]; } -- (BOOL)tableView:(NSTableView *)tableView_ -writeRowsWithIndexes:(NSIndexSet *)rowIndexes - toPasteboard:(NSPasteboard *)pboard { - if (rowIndexes.count == 1 && rowIndexes.firstIndex != 0) { - [pboard declareTypes:@[PB_ROW, NSFilesPromisePboardType] owner:nil]; - [pboard setString:@(rowIndexes.firstIndex).stringValue forType:PB_ROW]; - [pboard setPropertyList:@[@"enjoyable"] forType:NSFilesPromisePboardType]; - return YES; - } else if (rowIndexes.count == 1 && rowIndexes.firstIndex == 0) { - [pboard declareTypes:@[NSFilesPromisePboardType] owner:nil]; - [pboard setPropertyList:@[@"enjoyable"] forType:NSFilesPromisePboardType]; - return YES; - } else { - return NO; - } +- (void)mappingsViewController:(NJMappingsViewController *)mvc + choseMappingAtIndex:(NSInteger)idx { + [self activateMapping:self[idx]]; } @end