Missed userInfo keys.
[enjoyable.git] / Classes / NJMappingsController.m
1 //
2 // NJMappingsController.m
3 // Enjoy
4 //
5 // Created by Sam McCall on 4/05/09.
6 //
7
8 #import "NJMappingsController.h"
9
10 #import "NJMapping.h"
11 #import "NJMappingsController.h"
12 #import "NJOutput.h"
13 #import "NJEvents.h"
14
15 #define PB_ROW @"com.yukkurigames.Enjoyable.MappingRow"
16
17 @implementation NJMappingsController {
18 NSMutableArray *_mappings;
19 NJMapping *_manualMapping;
20 }
21
22 - (id)init {
23 if ((self = [super init])) {
24 _mappings = [[NSMutableArray alloc] init];
25 _currentMapping = [[NJMapping alloc] initWithName:@"(default)"];
26 _manualMapping = _currentMapping;
27 [_mappings addObject:_currentMapping];
28 }
29 return self;
30 }
31
32 - (void)awakeFromNib {
33 [tableView registerForDraggedTypes:@[PB_ROW, NSURLPboardType]];
34 [tableView setDraggingSourceOperationMask:NSDragOperationCopy forLocal:NO];
35 }
36
37 - (NJMapping *)objectForKeyedSubscript:(NSString *)name {
38 for (NJMapping *mapping in _mappings)
39 if ([name isEqualToString:mapping.name])
40 return mapping;
41 return nil;
42 }
43
44 - (NJMapping *)objectAtIndexedSubscript:(NSUInteger)idx {
45 return idx < _mappings.count ? _mappings[idx] : nil;
46 }
47
48 - (void)mappingsChanged {
49 [self save];
50 [tableView reloadData];
51 [self updateInterfaceForCurrentMapping];
52 [NSNotificationCenter.defaultCenter
53 postNotificationName:NJEventMappingListChanged
54 object:self
55 userInfo:@{ NJMappingListKey: _mappings,
56 NJMappingKey: _currentMapping }];
57 }
58
59 - (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state
60 objects:(__unsafe_unretained id [])buffer
61 count:(NSUInteger)len {
62 return [_mappings countByEnumeratingWithState:state
63 objects:buffer
64 count:len];
65 }
66
67 - (void)activateMappingForProcess:(NSRunningApplication *)app {
68 NJMapping *oldMapping = _manualMapping;
69 NSArray *names = app.possibleMappingNames;
70 BOOL found = NO;
71 for (NSString *name in names) {
72 NJMapping *mapping = self[name];
73 if (mapping) {
74 [self activateMapping:mapping];
75 found = YES;
76 break;
77 }
78 }
79
80 if (!found) {
81 [self activateMapping:oldMapping];
82 if ([oldMapping.name.lowercaseString isEqualToString:@"@application"]) {
83 oldMapping.name = app.bestMappingName;
84 [self mappingsChanged];
85 }
86 }
87 _manualMapping = oldMapping;
88 }
89
90 - (void)updateInterfaceForCurrentMapping {
91 NSUInteger selected = [_mappings indexOfObject:_currentMapping];
92 removeButton.enabled = selected != 0;
93 moveUp.enabled = selected > 1;
94 moveDown.enabled = selected && selected != _mappings.count - 1;
95 popoverActivate.title = _currentMapping.name;
96 [tableView selectRowIndexes:[NSIndexSet indexSetWithIndex:selected] byExtendingSelection:NO];
97 [NSUserDefaults.standardUserDefaults setInteger:selected forKey:@"selected"];
98 }
99
100 - (void)activateMapping:(NJMapping *)mapping {
101 if (!mapping)
102 mapping = _manualMapping;
103 if (mapping == _currentMapping)
104 return;
105 NSLog(@"Switching to mapping %@.", mapping.name);
106 _manualMapping = mapping;
107 _currentMapping = mapping;
108 [self updateInterfaceForCurrentMapping];
109 [NSNotificationCenter.defaultCenter
110 postNotificationName:NJEventMappingChanged
111 object:self
112 userInfo:@{ NJMappingKey : _currentMapping }];
113 }
114
115 - (IBAction)addPressed:(id)sender {
116 NJMapping *newMapping = [[NJMapping alloc] initWithName:@"Untitled"];
117 [_mappings addObject:newMapping];
118 [self activateMapping:newMapping];
119 [self mappingsChanged];
120 [tableView editColumn:0 row:_mappings.count - 1 withEvent:nil select:YES];
121 }
122
123 - (IBAction)removePressed:(id)sender {
124 if (tableView.selectedRow == 0)
125 return;
126
127 NSInteger selectedRow = tableView.selectedRow;
128 [_mappings removeObjectAtIndex:selectedRow];
129 [self activateMapping:_mappings[MIN(selectedRow, _mappings.count - 1)]];
130 [self mappingsChanged];
131 }
132
133 - (void)tableViewSelectionDidChange:(NSNotification *)notify {
134 [self activateMapping:self[tableView.selectedRow]];
135 }
136
137 - (id)tableView:(NSTableView *)view objectValueForTableColumn:(NSTableColumn *)column row:(NSInteger)index {
138 return self[index].name;
139 }
140
141 - (void)tableView:(NSTableView *)view
142 setObjectValue:(NSString *)obj
143 forTableColumn:(NSTableColumn *)col
144 row:(NSInteger)index {
145 self[index].name = obj;
146 [self mappingsChanged];
147 }
148
149 - (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView {
150 return _mappings.count;
151 }
152
153 - (BOOL)tableView:(NSTableView *)view shouldEditTableColumn:(NSTableColumn *)column row:(NSInteger)index {
154 return YES;
155 }
156
157 - (void)save {
158 NSLog(@"Saving mappings to defaults.");
159 NSMutableArray *ary = [[NSMutableArray alloc] initWithCapacity:_mappings.count];
160 for (NJMapping *mapping in _mappings)
161 [ary addObject:[mapping serialize]];
162 [NSUserDefaults.standardUserDefaults setObject:ary forKey:@"mappings"];
163 }
164
165 - (void)load {
166 NSUInteger selected = [NSUserDefaults.standardUserDefaults integerForKey:@"selected"];
167 NSArray *mappings = [NSUserDefaults.standardUserDefaults arrayForKey:@"mappings"];
168 [self loadAllFrom:mappings andActivate:selected];
169 }
170
171 - (void)loadAllFrom:(NSArray *)storedMappings andActivate:(NSUInteger)selected {
172 NSMutableArray* newMappings = [[NSMutableArray alloc] initWithCapacity:storedMappings.count];
173
174 // Requires two passes to deal with inter-mapping references. First make
175 // an empty mapping for each serialized mapping. Then, deserialize the
176 // data pointing to the empty mappings. Then merge that data back into
177 // its equivalent empty one, which is the one we finally use.
178 for (NSDictionary *storedMapping in storedMappings) {
179 NJMapping *mapping = [[NJMapping alloc] initWithName:storedMapping[@"name"]];
180 [newMappings addObject:mapping];
181 }
182
183 for (unsigned i = 0; i < storedMappings.count; ++i) {
184 NJMapping *realMapping = [[NJMapping alloc] initWithSerialization:storedMappings[i]
185 mappings:newMappings];
186 [newMappings[i] mergeEntriesFrom:realMapping];
187 }
188
189 if (newMappings.count) {
190 _mappings = newMappings;
191 if (selected >= newMappings.count)
192 selected = 0;
193 [self activateMapping:_mappings[selected]];
194 [self mappingsChanged];
195 }
196 }
197
198 - (void)addMappingWithContentsOfURL:(NSURL *)url {
199 NSWindow *window = popoverActivate.window;
200 NSError *error;
201 NJMapping *mapping = [NJMapping mappingWithContentsOfURL:url
202 mappings:_mappings
203 error:&error];
204
205 if (mapping && !error) {
206 NJMapping *mergeInto = self[mapping.name];
207 BOOL conflict = [mergeInto hasConflictWith:mapping];
208
209 if (conflict) {
210 NSAlert *conflictAlert = [[NSAlert alloc] init];
211 conflictAlert.messageText = @"Replace existing mappings?";
212 conflictAlert.informativeText =
213 [NSString stringWithFormat:
214 @"This file contains inputs you've already mapped in \"%@\". Do you "
215 @"want to merge them and replace your existing mappings, or import this "
216 @"as a separate mapping?", mapping.name];
217 [conflictAlert addButtonWithTitle:@"Merge"];
218 [conflictAlert addButtonWithTitle:@"Cancel"];
219 [conflictAlert addButtonWithTitle:@"New Mapping"];
220 NSInteger res = [conflictAlert runModal];
221 if (res == NSAlertSecondButtonReturn)
222 return;
223 else if (res == NSAlertThirdButtonReturn)
224 mergeInto = nil;
225 }
226
227 if (mergeInto) {
228 [mergeInto mergeEntriesFrom:mapping];
229 mapping = mergeInto;
230 } else {
231 [_mappings addObject:mapping];
232 }
233
234 [self activateMapping:mapping];
235 [self mappingsChanged];
236
237 if (conflict && !mergeInto) {
238 [tableView selectRowIndexes:[NSIndexSet indexSetWithIndex:_mappings.count - 1] byExtendingSelection:NO];
239 [tableView editColumn:0 row:_mappings.count - 1 withEvent:nil select:YES];
240 }
241 }
242
243 if (error) {
244 [window presentError:error
245 modalForWindow:window
246 delegate:nil
247 didPresentSelector:nil
248 contextInfo:nil];
249 }
250 }
251
252 - (void)importPressed:(id)sender {
253 NSOpenPanel *panel = [NSOpenPanel openPanel];
254 panel.allowedFileTypes = @[ @"enjoyable", @"json", @"txt" ];
255 NSWindow *window = NSApplication.sharedApplication.keyWindow;
256 [panel beginSheetModalForWindow:window
257 completionHandler:^(NSInteger result) {
258 if (result != NSFileHandlingPanelOKButton)
259 return;
260 [panel close];
261 [self addMappingWithContentsOfURL:panel.URL];
262 }];
263
264 }
265
266 - (void)exportPressed:(id)sender {
267 NSSavePanel *panel = [NSSavePanel savePanel];
268 panel.allowedFileTypes = @[ @"enjoyable" ];
269 NJMapping *mapping = _currentMapping;
270 panel.nameFieldStringValue = [mapping.name stringByFixingPathComponent];
271 NSWindow *window = NSApplication.sharedApplication.keyWindow;
272 [panel beginSheetModalForWindow:window
273 completionHandler:^(NSInteger result) {
274 if (result != NSFileHandlingPanelOKButton)
275 return;
276 [panel close];
277 NSError *error;
278 [mapping writeToURL:panel.URL error:&error];
279 if (error) {
280 [window presentError:error
281 modalForWindow:window
282 delegate:nil
283 didPresentSelector:nil
284 contextInfo:nil];
285 }
286 }];
287 }
288
289 - (IBAction)mappingPressed:(id)sender {
290 [popover showRelativeToRect:popoverActivate.bounds
291 ofView:popoverActivate
292 preferredEdge:NSMinXEdge];
293 }
294
295 - (void)popoverWillShow:(NSNotification *)notification {
296 popoverActivate.state = NSOnState;
297 }
298
299 - (void)popoverWillClose:(NSNotification *)notification {
300 popoverActivate.state = NSOffState;
301 }
302
303 - (IBAction)moveUpPressed:(id)sender {
304 if ([_mappings moveFirstwards:_currentMapping upTo:1])
305 [self mappingsChanged];
306 }
307
308 - (IBAction)moveDownPressed:(id)sender {
309 if ([_mappings moveLastwards:_currentMapping])
310 [self mappingsChanged];
311 }
312
313 - (BOOL)tableView:(NSTableView *)tableView_
314 acceptDrop:(id <NSDraggingInfo>)info
315 row:(NSInteger)row
316 dropOperation:(NSTableViewDropOperation)dropOperation {
317 NSPasteboard *pboard = [info draggingPasteboard];
318 if ([pboard.types containsObject:PB_ROW]) {
319 NSString *value = [pboard stringForType:PB_ROW];
320 NSUInteger srcRow = [value intValue];
321 [_mappings moveObjectAtIndex:srcRow toIndex:row];
322 [self mappingsChanged];
323 return YES;
324 } else if ([pboard.types containsObject:NSURLPboardType]) {
325 NSURL *url = [NSURL URLFromPasteboard:pboard];
326 NSError *error;
327 NJMapping *mapping = [NJMapping mappingWithContentsOfURL:url
328 mappings:_mappings
329 error:&error];
330 if (error) {
331 [tableView_ presentError:error];
332 return NO;
333 } else {
334 [_mappings insertObject:mapping atIndex:row];
335 [self mappingsChanged];
336 return YES;
337 }
338 } else {
339 return NO;
340 }
341 }
342
343 - (NSDragOperation)tableView:(NSTableView *)tableView_
344 validateDrop:(id <NSDraggingInfo>)info
345 proposedRow:(NSInteger)row
346 proposedDropOperation:(NSTableViewDropOperation)dropOperation {
347 NSPasteboard *pboard = [info draggingPasteboard];
348 if ([pboard.types containsObject:PB_ROW]) {
349 [tableView_ setDropRow:MAX(1, row) dropOperation:NSTableViewDropAbove];
350 return NSDragOperationMove;
351 } else if ([pboard.types containsObject:NSURLPboardType]) {
352 NSURL *url = [NSURL URLFromPasteboard:pboard];
353 if ([url.pathExtension isEqualToString:@"enjoyable"]) {
354 [tableView_ setDropRow:MAX(1, row) dropOperation:NSTableViewDropAbove];
355 return NSDragOperationCopy;
356 } else {
357 return NSDragOperationNone;
358 }
359 } else {
360 return NSDragOperationNone;
361 }
362 }
363
364 - (NSArray *)tableView:(NSTableView *)tableView_
365 namesOfPromisedFilesDroppedAtDestination:(NSURL *)dropDestination
366 forDraggedRowsWithIndexes:(NSIndexSet *)indexSet {
367 NJMapping *toSave = self[indexSet.firstIndex];
368 NSString *filename = [[toSave.name stringByFixingPathComponent]
369 stringByAppendingPathExtension:@"enjoyable"];
370 NSURL *dst = [dropDestination URLByAppendingPathComponent:filename];
371 dst = [NSFileManager.defaultManager generateUniqueURLWithBase:dst];
372 NSError *error;
373 if (![toSave writeToURL:dst error:&error]) {
374 [tableView_ presentError:error];
375 return @[];
376 } else {
377 return @[dst.lastPathComponent];
378 }
379 }
380
381 - (BOOL)tableView:(NSTableView *)tableView_
382 writeRowsWithIndexes:(NSIndexSet *)rowIndexes
383 toPasteboard:(NSPasteboard *)pboard {
384 if (rowIndexes.count == 1 && rowIndexes.firstIndex != 0) {
385 [pboard declareTypes:@[PB_ROW, NSFilesPromisePboardType] owner:nil];
386 [pboard setString:@(rowIndexes.firstIndex).stringValue forType:PB_ROW];
387 [pboard setPropertyList:@[@"enjoyable"] forType:NSFilesPromisePboardType];
388 return YES;
389 } else if (rowIndexes.count == 1 && rowIndexes.firstIndex == 0) {
390 [pboard declareTypes:@[NSFilesPromisePboardType] owner:nil];
391 [pboard setPropertyList:@[@"enjoyable"] forType:NSFilesPromisePboardType];
392 return YES;
393 } else {
394 return NO;
395 }
396 }
397
398 @end