2b08a52a3db05228e687b96a89d1e4e504b02d47
[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 [self postLoadProcess];
46 [NSNotificationCenter.defaultCenter
47 postNotificationName:NJEventMappingListChanged
48 object:self
49 userInfo:@{ NJMappingListKey: _mappings,
50 NJMappingKey: _currentMapping }];
51 [self.mvc changedActiveMappingToIndex:[_mappings indexOfObjectIdenticalTo:_currentMapping]];
52 }
53
54 - (void)mappingsChanged {
55 [self save];
56 [self mappingsSet];
57 }
58
59 - (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state
60 objects:(__unsafe_unretained id [])buffer
61 count:(NSUInteger)len {
62 return [_mappings countByEnumeratingWithState:state
63 objects:buffer
64 count:len];
65 }
66
67 - (void)activateMappingForProcess:(NSRunningApplication *)app {
68 NJMapping *oldMapping = _manualMapping;
69 NSArray *names = app.possibleMappingNames;
70 BOOL found = NO;
71 for (NSString *name in names) {
72 NJMapping *mapping = self[name];
73 if (mapping) {
74 [self activateMapping:mapping];
75 found = YES;
76 break;
77 }
78 }
79
80 if (!found) {
81 [self activateMapping:oldMapping];
82 if ([oldMapping.name.lowercaseString isEqualToString:@"@application"]
83 || [oldMapping.name.lowercaseString isEqualToString:
84 NSLocalizedString(@"@Application", nil).lowercaseString]) {
85 oldMapping.name = app.bestMappingName;
86 [self mappingsChanged];
87 }
88 }
89 _manualMapping = oldMapping;
90 }
91
92 - (void)activateMapping:(NJMapping *)mapping {
93 if (!mapping)
94 mapping = _manualMapping;
95 if (mapping == _currentMapping)
96 return;
97 NSLog(@"Switching to mapping %@.", mapping.name);
98 _manualMapping = mapping;
99 _currentMapping = mapping;
100 [self.mvc changedActiveMappingToIndex:[_mappings indexOfObjectIdenticalTo:_currentMapping]];
101 [NSNotificationCenter.defaultCenter
102 postNotificationName:NJEventMappingChanged
103 object:self
104 userInfo:@{ NJMappingKey : _currentMapping }];
105 }
106
107 - (void)save {
108 NSLog(@"Saving mappings to defaults.");
109 NSMutableArray *ary = [[NSMutableArray alloc] initWithCapacity:_mappings.count];
110 for (NJMapping *mapping in _mappings)
111 [ary addObject:[mapping serialize]];
112 [NSUserDefaults.standardUserDefaults setObject:ary forKey:@"mappings"];
113 }
114
115 - (void)postLoadProcess {
116 for (NJMapping *mapping in self)
117 [mapping postLoadProcess:self];
118 }
119
120 - (void)load {
121 NSUInteger selected = [NSUserDefaults.standardUserDefaults integerForKey:@"selected"];
122 NSArray *storedMappings = [NSUserDefaults.standardUserDefaults arrayForKey:@"mappings"];
123 NSMutableArray* newMappings = [[NSMutableArray alloc] initWithCapacity:storedMappings.count];
124
125 for (unsigned i = 0; i < storedMappings.count; ++i) {
126 NJMapping *mapping = [[NJMapping alloc] initWithSerialization:storedMappings[i]];
127 [newMappings addObject:mapping];
128 }
129
130
131 if (newMappings.count) {
132 _mappings = newMappings;
133 if (selected >= newMappings.count)
134 selected = 0;
135 [self.mvc reloadData];
136 [self activateMapping:_mappings[selected]];
137 [self mappingsSet];
138 }
139 }
140
141 - (NSInteger)indexOfMapping:(NJMapping *)mapping {
142 return [_mappings indexOfObjectIdenticalTo:mapping];
143 }
144
145 - (void)mergeMapping:(NJMapping *)mapping intoMapping:(NJMapping *)existing {
146 [existing mergeEntriesFrom:mapping];
147 [self mappingsChanged];
148 if (existing == _currentMapping) {
149 // FIXME: Hack to trigger updates when renaming.
150 _currentMapping = nil;
151 NJMapping *manual = _manualMapping;
152 [self activateMapping:existing];
153 _manualMapping = manual;
154 }
155 }
156
157 - (void)addMapping:(NJMapping *)mapping {
158 [self insertMapping:mapping atIndex:_mappings.count];
159 }
160
161 - (void)insertMapping:(NJMapping *)mapping atIndex:(NSInteger)idx {
162 [_mappings insertObject:mapping atIndex:idx];
163 [self mappingsChanged];
164 }
165
166 - (void)removeMappingAtIndex:(NSInteger)idx {
167 NSInteger currentIdx = [self indexOfMapping:_currentMapping];
168 [_mappings removeObjectAtIndex:idx];
169 [self activateMapping:self[MIN(currentIdx, _mappings.count - 1)]];
170 [self mappingsChanged];
171 }
172
173 - (void)moveMoveMappingFromIndex:(NSInteger)fromIdx toIndex:(NSInteger)toIdx {
174 [_mappings moveObjectAtIndex:fromIdx toIndex:toIdx];
175 [self mappingsChanged];
176 }
177
178 - (NSUInteger)count {
179 return _mappings.count;
180 }
181
182 - (void)mappingConflictDidResolve:(NSAlert *)alert
183 returnCode:(NSInteger)returnCode
184 contextInfo:(void *)contextInfo {
185 NSDictionary *userInfo = CFBridgingRelease(contextInfo);
186 NJMapping *oldMapping = userInfo[@"old mapping"];
187 NJMapping *newMapping = userInfo[@"new mapping"];
188 [alert.window orderOut:nil];
189 switch (returnCode) {
190 case NSAlertFirstButtonReturn: // Merge
191 [self mergeMapping:newMapping intoMapping:oldMapping];
192 [self activateMapping:oldMapping];
193 break;
194 case NSAlertThirdButtonReturn: // New Mapping
195 [self.mvc.mappingList beginUpdates];
196 [self addMapping:newMapping];
197 [self.mvc addedMappingAtIndex:_mappings.count - 1 startEditing:YES];
198 [self.mvc.mappingList endUpdates];
199 [self activateMapping:newMapping];
200 break;
201 default: // Cancel, other.
202 break;
203 }
204 }
205
206 - (void)promptForMapping:(NJMapping *)mapping atIndex:(NSInteger)idx {
207 NSWindow *window = NSApplication.sharedApplication.keyWindow;
208 NJMapping *mergeInto = self[mapping.name];
209 NSAlert *conflictAlert = [[NSAlert alloc] init];
210 conflictAlert.messageText = NSLocalizedString(@"import conflict prompt", @"Title of import conflict alert");
211 conflictAlert.informativeText =
212 [NSString stringWithFormat:NSLocalizedString(@"import conflict in %@", @"Explanation of import conflict"),
213 mapping.name];
214 [conflictAlert addButtonWithTitle:NSLocalizedString(@"import and merge", @"button to merge imported mappings")];
215 [conflictAlert addButtonWithTitle:NSLocalizedString(@"cancel import", @"button to cancel import")];
216 [conflictAlert addButtonWithTitle:NSLocalizedString(@"import new mapping", @"button to import as new mapping")];
217 [conflictAlert beginSheetModalForWindow:window
218 modalDelegate:self
219 didEndSelector:@selector(mappingConflictDidResolve:returnCode:contextInfo:)
220 contextInfo:(void *)CFBridgingRetain(@{ @"old mapping": mergeInto,
221 @"new mapping": mapping })];
222 }
223
224 - (NSInteger)numberOfMappings:(NJMappingsViewController *)mvc {
225 return self.count;
226 }
227
228 - (NJMapping *)mappingsViewController:(NJMappingsViewController *)mvc
229 mappingForIndex:(NSUInteger)idx {
230 return self[idx];
231 }
232
233 - (void)mappingsViewController:(NJMappingsViewController *)mvc
234 editedMappingAtIndex:(NSInteger)index {
235 [self mappingsChanged];
236 }
237
238 - (BOOL)mappingsViewController:(NJMappingsViewController *)mvc
239 canMoveMappingFromIndex:(NSInteger)fromIdx
240 toIndex:(NSInteger)toIdx {
241 return fromIdx != toIdx && fromIdx != 0 && toIdx != 0;
242 }
243
244 - (void)mappingsViewController:(NJMappingsViewController *)mvc
245 moveMappingFromIndex:(NSInteger)fromIdx
246 toIndex:(NSInteger)toIdx {
247 [mvc.mappingList beginUpdates];
248 [mvc.mappingList moveRowAtIndex:fromIdx toIndex:toIdx];
249 [self moveMoveMappingFromIndex:fromIdx toIndex:toIdx];
250 [mvc.mappingList endUpdates];
251 }
252
253 - (BOOL)mappingsViewController:(NJMappingsViewController *)mvc
254 canRemoveMappingAtIndex:(NSInteger)idx {
255 return idx != 0;
256 }
257
258 - (void)mappingsViewController:(NJMappingsViewController *)mvc
259 removeMappingAtIndex:(NSInteger)idx {
260 [mvc.mappingList beginUpdates];
261 [mvc removedMappingAtIndex:idx];
262 [self removeMappingAtIndex:idx];
263 [mvc.mappingList endUpdates];
264 }
265
266 - (BOOL)mappingsViewController:(NJMappingsViewController *)mvc
267 importMappingFromURL:(NSURL *)url
268 atIndex:(NSInteger)index
269 error:(NSError **)error {
270 NJMapping *mapping = [NJMapping mappingWithContentsOfURL:url
271 error:error];
272 if ([self[mapping.name] hasConflictWith:mapping]) {
273 [self promptForMapping:mapping atIndex:index];
274 } else if (self[mapping.name]) {
275 [self[mapping.name] mergeEntriesFrom:mapping];
276 } else if (mapping) {
277 [self insertMapping:mapping atIndex:index];
278 }
279 return !!mapping;
280 }
281
282 - (void)mappingsViewController:(NJMappingsViewController *)mvc
283 addMapping:(NJMapping *)mapping {
284 [mvc.mappingList beginUpdates];
285 [mvc addedMappingAtIndex:_mappings.count startEditing:YES];
286 [self addMapping:mapping];
287 [mvc.mappingList endUpdates];
288 [self activateMapping:mapping];
289 }
290
291 - (void)mappingsViewController:(NJMappingsViewController *)mvc
292 choseMappingAtIndex:(NSInteger)idx {
293 [self activateMapping:self[idx]];
294 }
295
296 @end