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