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