2 // NJMappingsController.m
5 // Created by Sam McCall on 4/05/09.
8 #import "NJMappingsController.h"
11 #import "NJMappingsController.h"
15 #define PB_ROW @"com.yukkurigames.Enjoyable.MappingRow"
17 @implementation NJMappingsController {
18 NSMutableArray *_mappings;
19 NJMapping *_manualMapping;
23 if ((self = [super init])) {
24 _mappings = [[NSMutableArray alloc] init];
25 _currentMapping = [[NJMapping alloc] initWithName:
26 NSLocalizedString(@"(default)", @"default name for first the mapping")];
27 _manualMapping = _currentMapping;
28 [_mappings addObject:_currentMapping];
33 - (void)awakeFromNib {
34 [tableView registerForDraggedTypes:@[PB_ROW, NSURLPboardType]];
35 [tableView setDraggingSourceOperationMask:NSDragOperationCopy forLocal:NO];
38 - (NJMapping *)objectForKeyedSubscript:(NSString *)name {
39 for (NJMapping *mapping in _mappings)
40 if ([name isEqualToString:mapping.name])
45 - (NJMapping *)objectAtIndexedSubscript:(NSUInteger)idx {
46 return idx < _mappings.count ? _mappings[idx] : nil;
49 - (void)mappingsChanged {
51 [tableView reloadData];
52 [self updateInterfaceForCurrentMapping];
53 [NSNotificationCenter.defaultCenter
54 postNotificationName:NJEventMappingListChanged
56 userInfo:@{ NJMappingListKey: _mappings,
57 NJMappingKey: _currentMapping }];
60 - (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state
61 objects:(__unsafe_unretained id [])buffer
62 count:(NSUInteger)len {
63 return [_mappings countByEnumeratingWithState:state
68 - (void)activateMappingForProcess:(NSRunningApplication *)app {
69 NJMapping *oldMapping = _manualMapping;
70 NSArray *names = app.possibleMappingNames;
72 for (NSString *name in names) {
73 NJMapping *mapping = self[name];
75 [self activateMapping:mapping];
82 [self activateMapping:oldMapping];
83 if ([oldMapping.name.lowercaseString isEqualToString:@"@application"]
84 || [oldMapping.name.lowercaseString isEqualToString:
85 NSLocalizedString(@"@Application", nil).lowercaseString]) {
86 oldMapping.name = app.bestMappingName;
87 [self mappingsChanged];
90 _manualMapping = oldMapping;
93 - (void)updateInterfaceForCurrentMapping {
94 NSUInteger selected = [_mappings indexOfObject:_currentMapping];
95 removeButton.enabled = selected != 0;
96 moveUp.enabled = selected > 1;
97 moveDown.enabled = selected && selected != _mappings.count - 1;
98 popoverActivate.title = _currentMapping.name;
99 [tableView selectRowIndexes:[NSIndexSet indexSetWithIndex:selected] byExtendingSelection:NO];
100 [NSUserDefaults.standardUserDefaults setInteger:selected forKey:@"selected"];
103 - (void)activateMapping:(NJMapping *)mapping {
105 mapping = _manualMapping;
106 if (mapping == _currentMapping)
108 NSLog(@"Switching to mapping %@.", mapping.name);
109 _manualMapping = mapping;
110 _currentMapping = mapping;
111 [self updateInterfaceForCurrentMapping];
112 [NSNotificationCenter.defaultCenter
113 postNotificationName:NJEventMappingChanged
115 userInfo:@{ NJMappingKey : _currentMapping }];
118 - (IBAction)addPressed:(id)sender {
119 NJMapping *newMapping = [[NJMapping alloc] init];
120 [_mappings addObject:newMapping];
121 [self activateMapping:newMapping];
122 [self mappingsChanged];
123 [tableView editColumn:0 row:_mappings.count - 1 withEvent:nil select:YES];
126 - (IBAction)removePressed:(id)sender {
127 if (tableView.selectedRow == 0)
130 NSInteger selectedRow = tableView.selectedRow;
131 [_mappings removeObjectAtIndex:selectedRow];
132 [self activateMapping:_mappings[MIN(selectedRow, _mappings.count - 1)]];
133 [self mappingsChanged];
136 - (void)tableViewSelectionDidChange:(NSNotification *)notify {
137 [self activateMapping:self[tableView.selectedRow]];
140 - (id)tableView:(NSTableView *)view objectValueForTableColumn:(NSTableColumn *)column row:(NSInteger)index {
141 return self[index].name;
144 - (void)tableView:(NSTableView *)view
145 setObjectValue:(NSString *)obj
146 forTableColumn:(NSTableColumn *)col
147 row:(NSInteger)index {
148 self[index].name = obj;
149 [self mappingsChanged];
152 - (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView {
153 return _mappings.count;
156 - (BOOL)tableView:(NSTableView *)view shouldEditTableColumn:(NSTableColumn *)column row:(NSInteger)index {
161 NSLog(@"Saving mappings to defaults.");
162 NSMutableArray *ary = [[NSMutableArray alloc] initWithCapacity:_mappings.count];
163 for (NJMapping *mapping in _mappings)
164 [ary addObject:[mapping serialize]];
165 [NSUserDefaults.standardUserDefaults setObject:ary forKey:@"mappings"];
169 NSUInteger selected = [NSUserDefaults.standardUserDefaults integerForKey:@"selected"];
170 NSArray *mappings = [NSUserDefaults.standardUserDefaults arrayForKey:@"mappings"];
171 [self loadAllFrom:mappings andActivate:selected];
174 - (void)loadAllFrom:(NSArray *)storedMappings andActivate:(NSUInteger)selected {
175 NSMutableArray* newMappings = [[NSMutableArray alloc] initWithCapacity:storedMappings.count];
177 // Requires two passes to deal with inter-mapping references. First make
178 // an empty mapping for each serialized mapping. Then, deserialize the
179 // data pointing to the empty mappings. Then merge that data back into
180 // its equivalent empty one, which is the one we finally use.
181 for (NSDictionary *storedMapping in storedMappings) {
182 NJMapping *mapping = [[NJMapping alloc] initWithName:storedMapping[@"name"]];
183 [newMappings addObject:mapping];
186 for (unsigned i = 0; i < storedMappings.count; ++i) {
187 NJMapping *realMapping = [[NJMapping alloc] initWithSerialization:storedMappings[i]
188 mappings:newMappings];
189 [newMappings[i] mergeEntriesFrom:realMapping];
192 if (newMappings.count) {
193 _mappings = newMappings;
194 if (selected >= newMappings.count)
196 [self activateMapping:_mappings[selected]];
197 [self mappingsChanged];
201 - (void)mappingConflictDidResolve:(NSAlert *)alert
202 returnCode:(NSInteger)returnCode
203 contextInfo:(void *)contextInfo {
204 NSDictionary *userInfo = CFBridgingRelease(contextInfo);
205 NJMapping *oldMapping = userInfo[@"old mapping"];
206 NJMapping *newMapping = userInfo[@"new mapping"];
207 switch (returnCode) {
208 case NSAlertFirstButtonReturn: // Merge
209 [oldMapping mergeEntriesFrom:newMapping];
210 [self activateMapping:oldMapping];
211 [self mappingsChanged];
213 case NSAlertThirdButtonReturn: // New Mapping
214 [_mappings addObject:newMapping];
215 [self activateMapping:newMapping];
216 [self mappingsChanged];
217 [self mappingPressed:alert];
218 [tableView selectRowIndexes:[NSIndexSet indexSetWithIndex:_mappings.count - 1] byExtendingSelection:NO];
219 [tableView editColumn:0 row:_mappings.count - 1 withEvent:nil select:YES];
221 default: // Cancel, other.
226 - (void)addMappingWithContentsOfURL:(NSURL *)url {
227 NSWindow *window = popoverActivate.window;
229 NJMapping *mapping = [NJMapping mappingWithContentsOfURL:url
233 if (mapping && !error) {
234 NJMapping *mergeInto = self[mapping.name];
235 if ([mergeInto hasConflictWith:mapping]) {
236 NSAlert *conflictAlert = [[NSAlert alloc] init];
237 conflictAlert.messageText = NSLocalizedString(@"import conflict prompt", @"Title of import conflict alert");
238 conflictAlert.informativeText =
239 [NSString stringWithFormat:NSLocalizedString(@"import conflict in %@", @"Explanation of import conflict"),
241 [conflictAlert addButtonWithTitle:NSLocalizedString(@"import and merge", @"button to merge imported mappings")];
242 [conflictAlert addButtonWithTitle:NSLocalizedString(@"cancel import", @"button to cancel import")];
243 [conflictAlert addButtonWithTitle:NSLocalizedString(@"import new mapping", @"button to import as new mapping")];
244 [conflictAlert beginSheetModalForWindow:popoverActivate.window
246 didEndSelector:@selector(mappingConflictDidResolve:returnCode:contextInfo:)
247 contextInfo:(void *)CFBridgingRetain(@{ @"old mapping": mergeInto,
248 @"new mapping": mapping })];
250 [_mappings addObject:mapping];
251 [self activateMapping:mapping];
252 [self mappingsChanged];
257 [window presentError:error
258 modalForWindow:window
260 didPresentSelector:nil
265 - (void)importPressed:(id)sender {
266 NSOpenPanel *panel = [NSOpenPanel openPanel];
267 panel.allowedFileTypes = @[ @"enjoyable", @"json", @"txt" ];
268 NSWindow *window = NSApplication.sharedApplication.keyWindow;
269 [panel beginSheetModalForWindow:window
270 completionHandler:^(NSInteger result) {
271 if (result != NSFileHandlingPanelOKButton)
274 [self addMappingWithContentsOfURL:panel.URL];
279 - (void)exportPressed:(id)sender {
280 NSSavePanel *panel = [NSSavePanel savePanel];
281 panel.allowedFileTypes = @[ @"enjoyable" ];
282 NJMapping *mapping = _currentMapping;
283 panel.nameFieldStringValue = [mapping.name stringByFixingPathComponent];
284 NSWindow *window = NSApplication.sharedApplication.keyWindow;
285 [panel beginSheetModalForWindow:window
286 completionHandler:^(NSInteger result) {
287 if (result != NSFileHandlingPanelOKButton)
291 [mapping writeToURL:panel.URL error:&error];
293 [window presentError:error
294 modalForWindow:window
296 didPresentSelector:nil
302 - (IBAction)mappingPressed:(id)sender {
303 [popover showRelativeToRect:popoverActivate.bounds
304 ofView:popoverActivate
305 preferredEdge:NSMinXEdge];
308 - (void)popoverWillShow:(NSNotification *)notification {
309 popoverActivate.state = NSOnState;
312 - (void)popoverWillClose:(NSNotification *)notification {
313 popoverActivate.state = NSOffState;
316 - (IBAction)moveUpPressed:(id)sender {
317 if ([_mappings moveFirstwards:_currentMapping upTo:1])
318 [self mappingsChanged];
321 - (IBAction)moveDownPressed:(id)sender {
322 if ([_mappings moveLastwards:_currentMapping])
323 [self mappingsChanged];
326 - (BOOL)tableView:(NSTableView *)tableView_
327 acceptDrop:(id <NSDraggingInfo>)info
329 dropOperation:(NSTableViewDropOperation)dropOperation {
330 NSPasteboard *pboard = [info draggingPasteboard];
331 if ([pboard.types containsObject:PB_ROW]) {
332 NSString *value = [pboard stringForType:PB_ROW];
333 NSUInteger srcRow = [value intValue];
334 [_mappings moveObjectAtIndex:srcRow toIndex:row];
335 [self mappingsChanged];
337 } else if ([pboard.types containsObject:NSURLPboardType]) {
338 NSURL *url = [NSURL URLFromPasteboard:pboard];
340 NJMapping *mapping = [NJMapping mappingWithContentsOfURL:url
344 [tableView_ presentError:error];
347 [_mappings insertObject:mapping atIndex:row];
348 [self mappingsChanged];
356 - (NSDragOperation)tableView:(NSTableView *)tableView_
357 validateDrop:(id <NSDraggingInfo>)info
358 proposedRow:(NSInteger)row
359 proposedDropOperation:(NSTableViewDropOperation)dropOperation {
360 NSPasteboard *pboard = [info draggingPasteboard];
361 if ([pboard.types containsObject:PB_ROW]) {
362 [tableView_ setDropRow:MAX(1, row) dropOperation:NSTableViewDropAbove];
363 return NSDragOperationMove;
364 } else if ([pboard.types containsObject:NSURLPboardType]) {
365 NSURL *url = [NSURL URLFromPasteboard:pboard];
366 if ([url.pathExtension isEqualToString:@"enjoyable"]) {
367 [tableView_ setDropRow:MAX(1, row) dropOperation:NSTableViewDropAbove];
368 return NSDragOperationCopy;
370 return NSDragOperationNone;
373 return NSDragOperationNone;
377 - (NSArray *)tableView:(NSTableView *)tableView_
378 namesOfPromisedFilesDroppedAtDestination:(NSURL *)dropDestination
379 forDraggedRowsWithIndexes:(NSIndexSet *)indexSet {
380 NJMapping *toSave = self[indexSet.firstIndex];
381 NSString *filename = [[toSave.name stringByFixingPathComponent]
382 stringByAppendingPathExtension:@"enjoyable"];
383 NSURL *dst = [dropDestination URLByAppendingPathComponent:filename];
384 dst = [NSFileManager.defaultManager generateUniqueURLWithBase:dst];
386 if (![toSave writeToURL:dst error:&error]) {
387 [tableView_ presentError:error];
390 return @[dst.lastPathComponent];
394 - (BOOL)tableView:(NSTableView *)tableView_
395 writeRowsWithIndexes:(NSIndexSet *)rowIndexes
396 toPasteboard:(NSPasteboard *)pboard {
397 if (rowIndexes.count == 1 && rowIndexes.firstIndex != 0) {
398 [pboard declareTypes:@[PB_ROW, NSFilesPromisePboardType] owner:nil];
399 [pboard setString:@(rowIndexes.firstIndex).stringValue forType:PB_ROW];
400 [pboard setPropertyList:@[@"enjoyable"] forType:NSFilesPromisePboardType];
402 } else if (rowIndexes.count == 1 && rowIndexes.firstIndex == 0) {
403 [pboard declareTypes:@[NSFilesPromisePboardType] owner:nil];
404 [pboard setPropertyList:@[@"enjoyable"] forType:NSFilesPromisePboardType];