2 // NJMappingsController.m
5 // Created by Sam McCall on 4/05/09.
8 #import "NJMappingsController.h"
11 #import "NJMappingsController.h"
13 #import "NJOutputController.h"
16 #define PB_ROW @"com.yukkurigames.Enjoyable.MappingRow"
18 @implementation NJMappingsController {
19 NSMutableArray *_mappings;
20 NJMapping *manualMapping;
24 if ((self = [super init])) {
25 _mappings = [[NSMutableArray alloc] init];
26 _currentMapping = [[NJMapping alloc] initWithName:@"(default)"];
27 manualMapping = _currentMapping;
28 [_mappings addObject:_currentMapping];
33 - (void)awakeFromNib {
34 [tableView registerForDraggedTypes:@[PB_ROW]];
37 - (NJMapping *)objectForKeyedSubscript:(NSString *)name {
38 for (NJMapping *mapping in _mappings)
39 if ([name isEqualToString:mapping.name])
44 - (NJMapping *)objectAtIndexedSubscript:(NSUInteger)idx {
45 return idx < _mappings.count ? _mappings[idx] : nil;
48 - (void)mappingsChanged {
50 [tableView reloadData];
51 popoverActivate.title = _currentMapping.name;
52 [self updateInterfaceForCurrentMapping];
53 [NSNotificationCenter.defaultCenter
54 postNotificationName:NJEventMappingListChanged
58 - (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state
59 objects:(__unsafe_unretained id [])buffer
60 count:(NSUInteger)len {
61 return [_mappings countByEnumeratingWithState:state
66 - (void)activateMappingForProcess:(NSString *)processName {
67 if ([manualMapping.name.lowercaseString isEqualToString:@"@application"]) {
68 manualMapping.name = processName;
69 [self mappingsChanged];
71 NJMapping *oldMapping = manualMapping;
72 NJMapping *newMapping = self[processName];
74 newMapping = oldMapping;
75 if (newMapping != _currentMapping)
76 [self activateMapping:newMapping];
77 manualMapping = oldMapping;
81 - (void)updateInterfaceForCurrentMapping {
82 NSUInteger selected = [_mappings indexOfObject:_currentMapping];
83 [removeButton setEnabled:selected != 0];
84 [moveDown setEnabled:selected && selected != _mappings.count - 1];
85 [moveUp setEnabled:selected > 1];
86 popoverActivate.title = _currentMapping.name;
87 [tableView selectRowIndexes:[NSIndexSet indexSetWithIndex:selected] byExtendingSelection:NO];
88 [NSUserDefaults.standardUserDefaults setInteger:selected forKey:@"selected"];
91 - (void)activateMapping:(NJMapping *)mapping {
93 mapping = manualMapping;
94 if (mapping == _currentMapping)
96 NSLog(@"Switching to mapping %@.", mapping.name);
97 manualMapping = mapping;
98 _currentMapping = mapping;
99 [self updateInterfaceForCurrentMapping];
100 [outputController loadCurrent];
101 [NSNotificationCenter.defaultCenter postNotificationName:NJEventMappingChanged
102 object:_currentMapping];
105 - (IBAction)addPressed:(id)sender {
106 NJMapping *newMapping = [[NJMapping alloc] initWithName:@"Untitled"];
107 [_mappings addObject:newMapping];
108 [self activateMapping:newMapping];
109 [self mappingsChanged];
110 [tableView editColumn:0 row:_mappings.count - 1 withEvent:nil select:YES];
113 - (IBAction)removePressed:(id)sender {
114 if (tableView.selectedRow == 0)
117 [_mappings removeObjectAtIndex:tableView.selectedRow];
118 [self activateMapping:_mappings[0]];
119 [self mappingsChanged];
122 -(void)tableViewSelectionDidChange:(NSNotification *)notify {
123 [self activateMapping:self[tableView.selectedRow]];
126 - (id)tableView:(NSTableView *)view objectValueForTableColumn:(NSTableColumn *)column row:(NSInteger)index {
127 return self[index].name;
130 - (void)tableView:(NSTableView *)view
131 setObjectValue:(NSString *)obj
132 forTableColumn:(NSTableColumn *)col
133 row:(NSInteger)index {
134 self[index].name = obj;
135 [self mappingsChanged];
138 - (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView {
139 return _mappings.count;
142 - (BOOL)tableView:(NSTableView *)view shouldEditTableColumn:(NSTableColumn *)column row:(NSInteger)index {
147 NSLog(@"Saving mappings to defaults.");
148 NSMutableArray *ary = [[NSMutableArray alloc] initWithCapacity:_mappings.count];
149 for (NJMapping *mapping in _mappings)
150 [ary addObject:[mapping serialize]];
151 [NSUserDefaults.standardUserDefaults setObject:ary forKey:@"mappings"];
155 NSUInteger selected = [NSUserDefaults.standardUserDefaults integerForKey:@"selected"];
156 NSArray *mappings = [NSUserDefaults.standardUserDefaults arrayForKey:@"mappings"];
157 [self loadAllFrom:mappings andActivate:selected];
160 - (void)loadAllFrom:(NSArray *)storedMappings andActivate:(NSUInteger)selected {
161 NSMutableArray* newMappings = [[NSMutableArray alloc] initWithCapacity:storedMappings.count];
163 // have to do two passes in case mapping1 refers to mapping2 via a NJOutputMapping
164 for (NSDictionary *storedMapping in storedMappings) {
165 NJMapping *mapping = [[NJMapping alloc] initWithName:storedMapping[@"name"]];
166 [newMappings addObject:mapping];
169 for (unsigned i = 0; i < storedMappings.count; ++i) {
170 NSDictionary *entries = storedMappings[i][@"entries"];
171 NJMapping *mapping = newMappings[i];
172 for (id key in entries) {
173 NJOutput *output = [NJOutput outputDeserialize:entries[key]
174 withMappings:newMappings];
176 mapping.entries[key] = output;
180 if (newMappings.count) {
181 _mappings = newMappings;
182 if (selected >= newMappings.count)
184 [self activateMapping:_mappings[selected]];
185 [self mappingsChanged];
189 - (NJMapping *)mappingWithURL:(NSURL *)url error:(NSError **)error {
190 NSInputStream *stream = [NSInputStream inputStreamWithURL:url];
192 NSDictionary *serialization = !*error
193 ? [NSJSONSerialization JSONObjectWithStream:stream options:0 error:error]
197 if (!([serialization isKindOfClass:NSDictionary.class]
198 && [serialization[@"name"] isKindOfClass:NSString.class]
199 && [serialization[@"entries"] isKindOfClass:NSDictionary.class])) {
200 *error = [NSError errorWithDomain:@"Enjoyable"
202 description:@"This isn't a valid mapping file."];
206 NSDictionary *entries = serialization[@"entries"];
207 NJMapping *mapping = [[NJMapping alloc] initWithName:serialization[@"name"]];
208 for (id key in entries) {
209 NSDictionary *value = entries[key];
210 if ([key isKindOfClass:NSString.class]) {
211 NJOutput *output = [NJOutput outputDeserialize:value
212 withMappings:_mappings];
214 mapping.entries[key] = output;
220 - (void)importPressed:(id)sender {
221 NSOpenPanel *panel = [NSOpenPanel openPanel];
222 panel.allowedFileTypes = @[ @"enjoyable", @"json", @"txt" ];
223 NSWindow *window = NSApplication.sharedApplication.keyWindow;
224 [panel beginSheetModalForWindow:window
225 completionHandler:^(NSInteger result) {
226 if (result != NSFileHandlingPanelOKButton)
231 NJMapping *mapping = [self mappingWithURL:panel.URL error:&error];
235 NJMapping *mergeInto = self[mapping.name];
236 for (id key in mapping.entries) {
237 if (mergeInto.entries[key]
238 && ![mergeInto.entries[key] isEqual:mapping.entries[key]]) {
245 NSAlert *conflictAlert = [[NSAlert alloc] init];
246 conflictAlert.messageText = @"Replace existing mappings?";
247 conflictAlert.informativeText =
248 [NSString stringWithFormat:
249 @"This file contains inputs you've already mapped in \"%@\". Do you "
250 @"want to merge them and replace your existing mappings, or import this "
251 @"as a separate mapping?", mapping.name];
252 [conflictAlert addButtonWithTitle:@"Merge"];
253 [conflictAlert addButtonWithTitle:@"Cancel"];
254 [conflictAlert addButtonWithTitle:@"New Mapping"];
255 NSInteger res = [conflictAlert runModal];
256 if (res == NSAlertSecondButtonReturn)
258 else if (res == NSAlertThirdButtonReturn)
263 [mergeInto.entries addEntriesFromDictionary:mapping.entries];
266 [_mappings addObject:mapping];
269 [self activateMapping:mapping];
270 [self mappingsChanged];
272 if (conflict && !mergeInto) {
273 [tableView selectRowIndexes:[NSIndexSet indexSetWithIndex:_mappings.count - 1] byExtendingSelection:NO];
274 [tableView editColumn:0 row:_mappings.count - 1 withEvent:nil select:YES];
279 [window presentError:error
280 modalForWindow:window
282 didPresentSelector:nil
289 - (void)exportPressed:(id)sender {
290 NSSavePanel *panel = [NSSavePanel savePanel];
291 panel.allowedFileTypes = @[ @"enjoyable" ];
292 NJMapping *mapping = _currentMapping;
293 panel.nameFieldStringValue = mapping.name;
294 NSWindow *window = NSApplication.sharedApplication.keyWindow;
295 [panel beginSheetModalForWindow:window
296 completionHandler:^(NSInteger result) {
297 if (result != NSFileHandlingPanelOKButton)
300 [NSProcessInfo.processInfo disableSuddenTermination];
302 NSDictionary *serialization = [mapping serialize];
303 NSData *json = [NSJSONSerialization dataWithJSONObject:serialization
304 options:NSJSONWritingPrettyPrinted
307 [json writeToURL:panel.URL options:NSDataWritingAtomic error:&error];
309 [NSProcessInfo.processInfo enableSuddenTermination];
311 [window presentError:error
312 modalForWindow:window
314 didPresentSelector:nil
320 - (IBAction)mappingPressed:(id)sender {
321 [popover showRelativeToRect:popoverActivate.bounds ofView:popoverActivate preferredEdge:NSMinXEdge];
324 - (void)popoverWillShow:(NSNotification *)notification {
325 popoverActivate.state = NSOnState;
328 - (void)popoverWillClose:(NSNotification *)notification {
329 popoverActivate.state = NSOffState;
332 - (IBAction)moveUpPressed:(id)sender {
333 NSUInteger idx = [_mappings indexOfObject:_currentMapping];
334 if (idx > 1 && idx != NSNotFound) {
335 [_mappings exchangeObjectAtIndex:idx withObjectAtIndex:idx - 1];
336 [self mappingsChanged];
340 - (IBAction)moveDownPressed:(id)sender {
341 NSUInteger idx = [_mappings indexOfObject:_currentMapping];
342 if (idx < _mappings.count - 1) {
343 [_mappings exchangeObjectAtIndex:idx withObjectAtIndex:idx + 1];
344 [self mappingsChanged];
348 - (BOOL)tableView:(NSTableView *)tableView
349 acceptDrop:(id <NSDraggingInfo>)info
351 dropOperation:(NSTableViewDropOperation)dropOperation {
352 NSPasteboard *pboard = [info draggingPasteboard];
353 if ([pboard.types containsObject:PB_ROW]) {
354 NSString *value = [pboard stringForType:PB_ROW];
355 NSUInteger srcRow = [value intValue];
356 [_mappings moveObjectAtIndex:srcRow toIndex:row];
357 [self mappingsChanged];
364 - (NSDragOperation)tableView:(NSTableView *)tableView_
365 validateDrop:(id <NSDraggingInfo>)info
366 proposedRow:(NSInteger)row
367 proposedDropOperation:(NSTableViewDropOperation)dropOperation {
368 NSPasteboard *pboard = [info draggingPasteboard];
369 if ([pboard.types containsObject:PB_ROW]) {
370 [tableView_ setDropRow:MAX(1, row) dropOperation:NSTableViewDropAbove];
371 return NSDragOperationGeneric;
373 return NSDragOperationNone;
377 - (BOOL)tableView:(NSTableView *)tableView
378 writeRowsWithIndexes:(NSIndexSet *)rowIndexes
379 toPasteboard:(NSPasteboard *)pboard {
380 if (rowIndexes.count == 1 && rowIndexes.firstIndex != 0) {
381 [pboard declareTypes:@[PB_ROW] owner:nil];
382 [pboard setString:@(rowIndexes.firstIndex).stringValue forType:PB_ROW];