531de12645c42b326076d634ecaf4e5f0d246e36
[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 switch (returnCode) {
151 case NSAlertFirstButtonReturn: // Merge
152 [oldMapping mergeEntriesFrom:newMapping];
153 _currentMapping = nil;
154 [self activateMapping:oldMapping];
155 [self mappingsChanged];
156 break;
157 case NSAlertThirdButtonReturn: // New Mapping
158 [self.mvc.mappingList beginUpdates];
159 [_mappings addObject:newMapping];
160 [self.mvc addedMappingAtIndex:_mappings.count - 1 startEditing:NO];
161 [self.mvc.mappingList endUpdates];
162 [self activateMapping:newMapping];
163 [self mappingsChanged];
164 break;
165 default: // Cancel, other.
166 break;
167 }
168 }
169
170 - (void)addOrMergeMapping:(NJMapping *)mapping {
171 [self addOrMergeMapping:mapping atIndex:-1];
172 }
173
174 - (void)addOrMergeMapping:(NJMapping *)mapping atIndex:(NSInteger)idx {
175 NSWindow *window = NSApplication.sharedApplication.keyWindow;
176 if (mapping) {
177 NJMapping *mergeInto = self[mapping.name];
178 if ([mergeInto hasConflictWith:mapping]) {
179 NSAlert *conflictAlert = [[NSAlert alloc] init];
180 conflictAlert.messageText = NSLocalizedString(@"import conflict prompt", @"Title of import conflict alert");
181 conflictAlert.informativeText =
182 [NSString stringWithFormat:NSLocalizedString(@"import conflict in %@", @"Explanation of import conflict"),
183 mapping.name];
184 [conflictAlert addButtonWithTitle:NSLocalizedString(@"import and merge", @"button to merge imported mappings")];
185 [conflictAlert addButtonWithTitle:NSLocalizedString(@"cancel import", @"button to cancel import")];
186 [conflictAlert addButtonWithTitle:NSLocalizedString(@"import new mapping", @"button to import as new mapping")];
187 [conflictAlert beginSheetModalForWindow:window
188 modalDelegate:self
189 didEndSelector:@selector(mappingConflictDidResolve:returnCode:contextInfo:)
190 contextInfo:(void *)CFBridgingRetain(@{ @"old mapping": mergeInto,
191 @"new mapping": mapping })];
192 } else if (mergeInto) {
193 [mergeInto mergeEntriesFrom:mapping];
194 [self activateMapping:mergeInto];
195 [self mappingsChanged];
196 } else {
197 if (idx == -1)
198 idx = _mappings.count - 1;
199 [self.mvc.mappingList beginUpdates];
200 [_mappings insertObject:mapping atIndex:idx];
201 [self.mvc addedMappingAtIndex:idx startEditing:NO];
202 [self.mvc.mappingList endUpdates];
203 [self activateMapping:mapping];
204 [self mappingsChanged];
205 }
206 }
207 }
208
209 - (NSInteger)numberOfMappings:(NJMappingsViewController *)dvc {
210 return _mappings.count;
211 }
212
213 - (NJMapping *)mappingsViewController:(NJMappingsViewController *)dvc
214 mappingForIndex:(NSUInteger)idx {
215 return _mappings[idx];
216 }
217
218 - (void)mappingsViewController:(NJMappingsViewController *)mvc
219 editedMappingAtIndex:(NSInteger)index {
220 [self mappingsChanged];
221 }
222
223 - (BOOL)mappingsViewController:(NJMappingsViewController *)mvc
224 canMoveMappingFromIndex:(NSInteger)fromIdx
225 toIndex:(NSInteger)toIdx {
226 return fromIdx != toIdx && fromIdx != 0 && toIdx != 0 && toIdx < (NSInteger)_mappings.count;
227 }
228
229 - (void)mappingsViewController:(NJMappingsViewController *)mvc
230 moveMappingFromIndex:(NSInteger)fromIdx
231 toIndex:(NSInteger)toIdx {
232 [_mappings moveObjectAtIndex:fromIdx toIndex:toIdx];
233 [self mappingsChanged];
234 }
235
236 - (BOOL)mappingsViewController:(NJMappingsViewController *)mvc
237 canRemoveMappingAtIndex:(NSInteger)idx {
238 return idx != 0;
239 }
240
241 - (void)mappingsViewController:(NJMappingsViewController *)mvc
242 removeMappingAtIndex:(NSInteger)idx {
243 NJMapping *old = self[idx];
244 [self.mvc.mappingList beginUpdates];
245 [_mappings removeObjectAtIndex:idx];
246 [self.mvc removedMappingAtIndex:idx];
247 [self.mvc.mappingList endUpdates];
248 if (old == _currentMapping)
249 [self activateMapping:self[MIN(idx, _mappings.count - 1)]];
250 [self mappingsChanged];
251 }
252
253 - (BOOL)mappingsViewController:(NJMappingsViewController *)mvc
254 importMappingFromURL:(NSURL *)url
255 atIndex:(NSInteger)index
256 error:(NSError **)error {
257 NJMapping *mapping = [NJMapping mappingWithContentsOfURL:url
258 mappings:_mappings
259 error:error];
260 [self addOrMergeMapping:mapping atIndex:index];
261 return !!mapping;
262 }
263
264 - (void)mappingsViewController:(NJMappingsViewController *)mvc
265 addMapping:(NJMapping *)mapping {
266 [self.mvc.mappingList beginUpdates];
267 [_mappings addObject:mapping];
268 [self.mvc addedMappingAtIndex:_mappings.count - 1 startEditing:YES];
269 [self.mvc.mappingList endUpdates];
270 [self activateMapping:mapping];
271 [self mappingsChanged];
272 }
273
274 - (void)mappingsViewController:(NJMappingsViewController *)mvc
275 choseMappingAtIndex:(NSInteger)idx {
276 [self activateMapping:self[idx]];
277 }
278
279 @end