4859970fc63ff07b81788bf655978ec33d63f019
[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:
26 NSLocalizedString(@"(default)", @"default name for first the mapping")];
27 _manualMapping = _currentMapping;
28 [_mappings addObject:_currentMapping];
29 }
30 return self;
31 }
32
33 - (NJMapping *)objectForKeyedSubscript:(NSString *)name {
34 for (NJMapping *mapping in _mappings)
35 if ([name isEqualToString:mapping.name])
36 return mapping;
37 return nil;
38 }
39
40 - (NJMapping *)objectAtIndexedSubscript:(NSUInteger)idx {
41 return idx < _mappings.count ? _mappings[idx] : nil;
42 }
43
44 - (void)mappingsSet {
45 [NSNotificationCenter.defaultCenter
46 postNotificationName:NJEventMappingListChanged
47 object:self
48 userInfo:@{ NJMappingListKey: _mappings,
49 NJMappingKey: _currentMapping }];
50 [self.mvc changedActiveMappingToIndex:[_mappings indexOfObjectIdenticalTo:_currentMapping]];
51 }
52
53 - (void)mappingsChanged {
54 [self save];
55 [self mappingsSet];
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.lowercaseString isEqualToString:
83 NSLocalizedString(@"@Application", nil).lowercaseString]) {
84 oldMapping.name = app.bestMappingName;
85 [self mappingsChanged];
86 }
87 }
88 _manualMapping = oldMapping;
89 }
90
91 - (void)activateMapping:(NJMapping *)mapping {
92 if (!mapping)
93 mapping = _manualMapping;
94 if (mapping == _currentMapping)
95 return;
96 NSLog(@"Switching to mapping %@.", mapping.name);
97 _manualMapping = mapping;
98 _currentMapping = mapping;
99 [self.mvc changedActiveMappingToIndex:[_mappings indexOfObjectIdenticalTo:_currentMapping]];
100 [NSNotificationCenter.defaultCenter
101 postNotificationName:NJEventMappingChanged
102 object:self
103 userInfo:@{ NJMappingKey : _currentMapping }];
104 }
105
106 - (void)save {
107 NSLog(@"Saving mappings to defaults.");
108 NSMutableArray *ary = [[NSMutableArray alloc] initWithCapacity:_mappings.count];
109 for (NJMapping *mapping in _mappings)
110 [ary addObject:[mapping serialize]];
111 [NSUserDefaults.standardUserDefaults setObject:ary forKey:@"mappings"];
112 }
113
114 - (void)load {
115 NSUInteger selected = [NSUserDefaults.standardUserDefaults integerForKey:@"selected"];
116 NSArray *storedMappings = [NSUserDefaults.standardUserDefaults arrayForKey:@"mappings"];
117 NSMutableArray* newMappings = [[NSMutableArray alloc] initWithCapacity:storedMappings.count];
118
119 // Requires two passes to deal with inter-mapping references. First make
120 // an empty mapping for each serialized mapping. Then, deserialize the
121 // data pointing to the empty mappings. Then merge that data back into
122 // its equivalent empty one, which is the one we finally use.
123 for (NSDictionary *storedMapping in storedMappings) {
124 NJMapping *mapping = [[NJMapping alloc] initWithName:storedMapping[@"name"]];
125 [newMappings addObject:mapping];
126 }
127
128 for (unsigned i = 0; i < storedMappings.count; ++i) {
129 NJMapping *realMapping = [[NJMapping alloc] initWithSerialization:storedMappings[i]
130 mappings:newMappings];
131 [newMappings[i] mergeEntriesFrom:realMapping];
132 }
133
134 if (newMappings.count) {
135 _mappings = newMappings;
136 if (selected >= newMappings.count)
137 selected = 0;
138 [self.mvc reloadData];
139 [self activateMapping:_mappings[selected]];
140 [self mappingsSet];
141 }
142 }
143
144 - (void)mappingConflictDidResolve:(NSAlert *)alert
145 returnCode:(NSInteger)returnCode
146 contextInfo:(void *)contextInfo {
147 NSDictionary *userInfo = CFBridgingRelease(contextInfo);
148 NJMapping *oldMapping = userInfo[@"old mapping"];
149 NJMapping *newMapping = userInfo[@"new mapping"];
150 [alert.window orderOut:nil];
151 switch (returnCode) {
152 case NSAlertFirstButtonReturn: // Merge
153 [oldMapping mergeEntriesFrom:newMapping];
154 _currentMapping = nil;
155 [self activateMapping:oldMapping];
156 [self mappingsChanged];
157 break;
158 case NSAlertThirdButtonReturn: // New Mapping
159 [self.mvc.mappingList beginUpdates];
160 [_mappings addObject:newMapping];
161 [self.mvc addedMappingAtIndex:_mappings.count - 1 startEditing:YES];
162 [self.mvc.mappingList endUpdates];
163 [self activateMapping:newMapping];
164 [self mappingsChanged];
165 break;
166 default: // Cancel, other.
167 break;
168 }
169 }
170
171 - (void)addOrMergeMapping:(NJMapping *)mapping {
172 [self addOrMergeMapping:mapping atIndex:-1];
173 }
174
175 - (void)addOrMergeMapping:(NJMapping *)mapping atIndex:(NSInteger)idx {
176 NSWindow *window = NSApplication.sharedApplication.keyWindow;
177 if (mapping) {
178 NJMapping *mergeInto = self[mapping.name];
179 if ([mergeInto hasConflictWith:mapping]) {
180 NSAlert *conflictAlert = [[NSAlert alloc] init];
181 conflictAlert.messageText = NSLocalizedString(@"import conflict prompt", @"Title of import conflict alert");
182 conflictAlert.informativeText =
183 [NSString stringWithFormat:NSLocalizedString(@"import conflict in %@", @"Explanation of import conflict"),
184 mapping.name];
185 [conflictAlert addButtonWithTitle:NSLocalizedString(@"import and merge", @"button to merge imported mappings")];
186 [conflictAlert addButtonWithTitle:NSLocalizedString(@"cancel import", @"button to cancel import")];
187 [conflictAlert addButtonWithTitle:NSLocalizedString(@"import new mapping", @"button to import as new mapping")];
188 [conflictAlert beginSheetModalForWindow:window
189 modalDelegate:self
190 didEndSelector:@selector(mappingConflictDidResolve:returnCode:contextInfo:)
191 contextInfo:(void *)CFBridgingRetain(@{ @"old mapping": mergeInto,
192 @"new mapping": mapping })];
193 } else if (mergeInto) {
194 [mergeInto mergeEntriesFrom:mapping];
195 [self activateMapping:mergeInto];
196 [self mappingsChanged];
197 } else {
198 if (idx == -1)
199 idx = _mappings.count;
200 [self.mvc.mappingList beginUpdates];
201 [_mappings insertObject:mapping atIndex:idx];
202 [self.mvc addedMappingAtIndex:idx startEditing:NO];
203 [self.mvc.mappingList endUpdates];
204 [self activateMapping:mapping];
205 [self mappingsChanged];
206 }
207 }
208 }
209
210 - (NSInteger)numberOfMappings:(NJMappingsViewController *)dvc {
211 return _mappings.count;
212 }
213
214 - (NJMapping *)mappingsViewController:(NJMappingsViewController *)dvc
215 mappingForIndex:(NSUInteger)idx {
216 return _mappings[idx];
217 }
218
219 - (void)mappingsViewController:(NJMappingsViewController *)mvc
220 editedMappingAtIndex:(NSInteger)index {
221 [self mappingsChanged];
222 }
223
224 - (BOOL)mappingsViewController:(NJMappingsViewController *)mvc
225 canMoveMappingFromIndex:(NSInteger)fromIdx
226 toIndex:(NSInteger)toIdx {
227 return fromIdx != toIdx && fromIdx != 0 && toIdx != 0 && toIdx < (NSInteger)_mappings.count;
228 }
229
230 - (void)mappingsViewController:(NJMappingsViewController *)mvc
231 moveMappingFromIndex:(NSInteger)fromIdx
232 toIndex:(NSInteger)toIdx {
233 [_mappings moveObjectAtIndex:fromIdx toIndex:toIdx];
234 [self mappingsChanged];
235 }
236
237 - (BOOL)mappingsViewController:(NJMappingsViewController *)mvc
238 canRemoveMappingAtIndex:(NSInteger)idx {
239 return idx != 0;
240 }
241
242 - (void)mappingsViewController:(NJMappingsViewController *)mvc
243 removeMappingAtIndex:(NSInteger)idx {
244 NJMapping *old = self[idx];
245 [self.mvc.mappingList beginUpdates];
246 [_mappings removeObjectAtIndex:idx];
247 [self.mvc removedMappingAtIndex:idx];
248 [self.mvc.mappingList endUpdates];
249 if (old == _currentMapping)
250 [self activateMapping:self[MIN(idx, _mappings.count - 1)]];
251 [self mappingsChanged];
252 }
253
254 - (BOOL)mappingsViewController:(NJMappingsViewController *)mvc
255 importMappingFromURL:(NSURL *)url
256 atIndex:(NSInteger)index
257 error:(NSError **)error {
258 NJMapping *mapping = [NJMapping mappingWithContentsOfURL:url
259 mappings:_mappings
260 error:error];
261 [self addOrMergeMapping:mapping atIndex:index];
262 return !!mapping;
263 }
264
265 - (void)mappingsViewController:(NJMappingsViewController *)mvc
266 addMapping:(NJMapping *)mapping {
267 [self.mvc.mappingList beginUpdates];
268 [_mappings addObject:mapping];
269 [self.mvc addedMappingAtIndex:_mappings.count - 1 startEditing:YES];
270 [self.mvc.mappingList endUpdates];
271 [self activateMapping:mapping];
272 [self mappingsChanged];
273 }
274
275 - (void)mappingsViewController:(NJMappingsViewController *)mvc
276 choseMappingAtIndex:(NSInteger)idx {
277 [self activateMapping:self[idx]];
278 }
279
280 @end