Fix an off-by-one error. Make the popover work correctly when conflicts occur.
[enjoyable.git] / Classes / NJMappingsController.m
index d7310c0..4859970 100644 (file)
 - (id)init {
     if ((self = [super init])) {
         _mappings = [[NSMutableArray alloc] init];
-        _currentMapping = [[NJMapping alloc] initWithName:@"(default)"];
+        _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])
     return idx < _mappings.count ? _mappings[idx] : nil;
 }
 
-- (void)mappingsChanged {
-    [self save];
-    [tableView reloadData];
-    [self updateInterfaceForCurrentMapping];
+- (void)mappingsSet {
     [NSNotificationCenter.defaultCenter
         postNotificationName:NJEventMappingListChanged
                       object:self
                     userInfo:@{ NJMappingListKey: _mappings,
                                 NJMappingKey: _currentMapping }];
+    [self.mvc changedActiveMappingToIndex:[_mappings indexOfObjectIdenticalTo:_currentMapping]];
+}
+
+- (void)mappingsChanged {
+    [self save];
+    [self mappingsSet];
 }
 
 - (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state
@@ -79,7 +78,9 @@
 
     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.enabled = selected != 0;
-    moveUp.enabled = selected > 1;
-    moveDown.enabled = selected && selected != _mappings.count - 1;
-    popoverActivate.title = _currentMapping.name;
-    [tableView selectRowIndexes:[NSIndexSet indexSetWithIndex:selected] byExtendingSelection:NO];
-    [NSUserDefaults.standardUserDefaults setInteger:selected forKey:@"selected"];
-}
-
 - (void)activateMapping:(NJMapping *)mapping {
     if (!mapping)
         mapping = _manualMapping;
     NSLog(@"Switching to mapping %@.", mapping.name);
     _manualMapping = mapping;
     _currentMapping = mapping;
-    [self updateInterfaceForCurrentMapping];
+    [self.mvc changedActiveMappingToIndex:[_mappings indexOfObjectIdenticalTo:_currentMapping]];
     [NSNotificationCenter.defaultCenter
          postNotificationName:NJEventMappingChanged
-         object:self
-         userInfo:@{ @"mapping": _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;
+                       object:self
+                     userInfo:@{ NJMappingKey : _currentMapping }];
 }
 
 - (void)save {
 
 - (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];
 
     // Requires two passes to deal with inter-mapping references. First make
         _mappings = newMappings;
         if (selected >= newMappings.count)
             selected = 0;
+        [self.mvc reloadData];
         [self activateMapping:_mappings[selected]];
-        [self mappingsChanged];
+        [self mappingsSet];
     }
 }
 
-- (void)addMappingWithContentsOfURL:(NSURL *)url {
-    NSWindow *window = popoverActivate.window;
-    NSError *error;
-    NJMapping *mapping = [NJMapping mappingWithContentsOfURL:url
-                                                    mappings:_mappings
-                                                       error:&error];
-    
-    if (mapping && !error) {
+- (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;
+    }
+}
+
+- (void)addOrMergeMapping:(NJMapping *)mapping {
+    [self addOrMergeMapping:mapping atIndex:-1];
+}
+
+- (void)addOrMergeMapping:(NJMapping *)mapping atIndex:(NSInteger)idx {
+    NSWindow *window = NSApplication.sharedApplication.keyWindow;
+    if (mapping) {
         NJMapping *mergeInto = self[mapping.name];
-        BOOL conflict = [mergeInto hasConflictWith:mapping];
-        
-        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) {
+            [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];
-            mapping = mergeInto;
+            [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 {
-    if ([_mappings moveFirstwards:_currentMapping upTo: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 {
-    if ([_mappings moveLastwards:_currentMapping])
-        [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 <NSDraggingInfo>)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 <NSDraggingInfo>)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