5d861868ea572afe7493ce06ff773546369654af
[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 // Requires two passes to deal with inter-mapping references. First make
171 // an empty mapping for each serialized mapping. Then, deserialize the
172 // data pointing to the empty mappings. Then merge that data back into
173 // its equivalent empty one, which is the one we finally use.
174 for (NSDictionary *storedMapping in storedMappings) {
175 NJMapping *mapping = [[NJMapping alloc] initWithName:storedMapping[@"name"]];
176 [newMappings addObject:mapping];
177 }
178
179 for (unsigned i = 0; i < storedMappings.count; ++i) {
180 NJMapping *realMapping = [[NJMapping alloc] initWithSerialization:storedMappings[i]
181 mappings:newMappings];
182 [newMappings[i] mergeEntriesFrom:realMapping];
183 }
184
185 if (newMappings.count) {
186 _mappings = newMappings;
187 if (selected >= newMappings.count)
188 selected = 0;
189 [self activateMapping:_mappings[selected]];
190 [self mappingsChanged];
191 }
192 }
193
194 - (void)addMappingWithContentsOfURL:(NSURL *)url {
195 NSWindow *window = popoverActivate.window;
196 NSError *error;
197 NJMapping *mapping = [NJMapping mappingWithContentsOfURL:url
198 mappings:_mappings
199 error:&error];
200
201 if (mapping && !error) {
202 NJMapping *mergeInto = self[mapping.name];
203 BOOL conflict = [mergeInto hasConflictWith:mapping];
204
205 if (conflict) {
206 NSAlert *conflictAlert = [[NSAlert alloc] init];
207 conflictAlert.messageText = @"Replace existing mappings?";
208 conflictAlert.informativeText =
209 [NSString stringWithFormat:
210 @"This file contains inputs you've already mapped in \"%@\". Do you "
211 @"want to merge them and replace your existing mappings, or import this "
212 @"as a separate mapping?", mapping.name];
213 [conflictAlert addButtonWithTitle:@"Merge"];
214 [conflictAlert addButtonWithTitle:@"Cancel"];
215 [conflictAlert addButtonWithTitle:@"New Mapping"];
216 NSInteger res = [conflictAlert runModal];
217 if (res == NSAlertSecondButtonReturn)
218 return;
219 else if (res == NSAlertThirdButtonReturn)
220 mergeInto = nil;
221 }
222
223 if (mergeInto) {
224 [mergeInto mergeEntriesFrom:mapping];
225 mapping = mergeInto;
226 } else {
227 [_mappings addObject:mapping];
228 }
229
230 [self activateMapping:mapping];
231 [self mappingsChanged];
232
233 if (conflict && !mergeInto) {
234 [tableView selectRowIndexes:[NSIndexSet indexSetWithIndex:_mappings.count - 1] byExtendingSelection:NO];
235 [tableView editColumn:0 row:_mappings.count - 1 withEvent:nil select:YES];
236 }
237 }
238
239 if (error) {
240 [window presentError:error
241 modalForWindow:window
242 delegate:nil
243 didPresentSelector:nil
244 contextInfo:nil];
245 }
246 }
247
248 - (void)importPressed:(id)sender {
249 NSOpenPanel *panel = [NSOpenPanel openPanel];
250 panel.allowedFileTypes = @[ @"enjoyable", @"json", @"txt" ];
251 NSWindow *window = NSApplication.sharedApplication.keyWindow;
252 [panel beginSheetModalForWindow:window
253 completionHandler:^(NSInteger result) {
254 if (result != NSFileHandlingPanelOKButton)
255 return;
256 [panel close];
257 [self addMappingWithContentsOfURL:panel.URL];
258 }];
259
260 }
261
262 - (void)exportPressed:(id)sender {
263 NSSavePanel *panel = [NSSavePanel savePanel];
264 panel.allowedFileTypes = @[ @"enjoyable" ];
265 NJMapping *mapping = _currentMapping;
266 panel.nameFieldStringValue = [mapping.name stringByFixingPathComponent];
267 NSWindow *window = NSApplication.sharedApplication.keyWindow;
268 [panel beginSheetModalForWindow:window
269 completionHandler:^(NSInteger result) {
270 if (result != NSFileHandlingPanelOKButton)
271 return;
272 [panel close];
273 NSError *error;
274 [mapping writeToURL:panel.URL error:&error];
275 if (error) {
276 [window presentError:error
277 modalForWindow:window
278 delegate:nil
279 didPresentSelector:nil
280 contextInfo:nil];
281 }
282 }];
283 }
284
285 - (IBAction)mappingPressed:(id)sender {
286 [popover showRelativeToRect:popoverActivate.bounds
287 ofView:popoverActivate
288 preferredEdge:NSMinXEdge];
289 }
290
291 - (void)popoverWillShow:(NSNotification *)notification {
292 popoverActivate.state = NSOnState;
293 }
294
295 - (void)popoverWillClose:(NSNotification *)notification {
296 popoverActivate.state = NSOffState;
297 }
298
299 - (IBAction)moveUpPressed:(id)sender {
300 if ([_mappings moveFirstwards:_currentMapping upTo:1])
301 [self mappingsChanged];
302 }
303
304 - (IBAction)moveDownPressed:(id)sender {
305 if ([_mappings moveLastwards:_currentMapping])
306 [self mappingsChanged];
307 }
308
309 - (BOOL)tableView:(NSTableView *)tableView_
310 acceptDrop:(id <NSDraggingInfo>)info
311 row:(NSInteger)row
312 dropOperation:(NSTableViewDropOperation)dropOperation {
313 NSPasteboard *pboard = [info draggingPasteboard];
314 if ([pboard.types containsObject:PB_ROW]) {
315 NSString *value = [pboard stringForType:PB_ROW];
316 NSUInteger srcRow = [value intValue];
317 [_mappings moveObjectAtIndex:srcRow toIndex:row];
318 [self mappingsChanged];
319 return YES;
320 } else if ([pboard.types containsObject:NSURLPboardType]) {
321 NSURL *url = [NSURL URLFromPasteboard:pboard];
322 NSError *error;
323 NJMapping *mapping = [NJMapping mappingWithContentsOfURL:url
324 mappings:_mappings
325 error:&error];
326 if (error) {
327 [tableView_ presentError:error];
328 return NO;
329 } else {
330 [_mappings insertObject:mapping atIndex:row];
331 [self mappingsChanged];
332 return YES;
333 }
334 } else {
335 return NO;
336 }
337 }
338
339 - (NSDragOperation)tableView:(NSTableView *)tableView_
340 validateDrop:(id <NSDraggingInfo>)info
341 proposedRow:(NSInteger)row
342 proposedDropOperation:(NSTableViewDropOperation)dropOperation {
343 NSPasteboard *pboard = [info draggingPasteboard];
344 if ([pboard.types containsObject:PB_ROW]) {
345 [tableView_ setDropRow:MAX(1, row) dropOperation:NSTableViewDropAbove];
346 return NSDragOperationMove;
347 } else if ([pboard.types containsObject:NSURLPboardType]) {
348 NSURL *url = [NSURL URLFromPasteboard:pboard];
349 if ([url.pathExtension isEqualToString:@"enjoyable"]) {
350 [tableView_ setDropRow:MAX(1, row) dropOperation:NSTableViewDropAbove];
351 return NSDragOperationCopy;
352 } else {
353 return NSDragOperationNone;
354 }
355 } else {
356 return NSDragOperationNone;
357 }
358 }
359
360 - (NSArray *)tableView:(NSTableView *)tableView_
361 namesOfPromisedFilesDroppedAtDestination:(NSURL *)dropDestination
362 forDraggedRowsWithIndexes:(NSIndexSet *)indexSet {
363 NJMapping *toSave = self[indexSet.firstIndex];
364 NSString *filename = [[toSave.name stringByFixingPathComponent]
365 stringByAppendingPathExtension:@"enjoyable"];
366 NSURL *dst = [dropDestination URLByAppendingPathComponent:filename];
367 dst = [NSFileManager.defaultManager generateUniqueURLWithBase:dst];
368 NSError *error;
369 if (![toSave writeToURL:dst error:&error]) {
370 [tableView_ presentError:error];
371 return @[];
372 } else {
373 return @[dst.lastPathComponent];
374 }
375 }
376
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, NSFilesPromisePboardType] owner:nil];
382 [pboard setString:@(rowIndexes.firstIndex).stringValue forType:PB_ROW];
383 [pboard setPropertyList:@[@"enjoyable"] forType:NSFilesPromisePboardType];
384 return YES;
385 } else if (rowIndexes.count == 1 && rowIndexes.firstIndex == 0) {
386 [pboard declareTypes:@[NSFilesPromisePboardType] owner:nil];
387 [pboard setPropertyList:@[@"enjoyable"] forType:NSFilesPromisePboardType];
388 return YES;
389 } else {
390 return NO;
391 }
392 }
393
394 @end