/* ** iTermKeyBindingMgr.m ** ** Copyright (c) 2002, 2003, 2004 ** ** Author: Ujwal S. Setlur ** ** Project: iTerm ** ** Description: implements the key binding manager. ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program; if not, write to the Free Software ** Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ #import static iTermKeyBindingMgr *singleInstance = nil; @implementation iTermKeyBindingMgr + (id) singleInstance; { if(singleInstance == nil) { singleInstance = [[iTermKeyBindingMgr alloc] init]; } return (singleInstance); } - (id) init { self = [super init]; if(!self) return (nil); profiles = [[NSMutableDictionary alloc] init]; return (self); } - (void) dealloc { [profiles release]; [super dealloc]; } - (NSDictionary *) profiles { return (profiles); } - (void) setProfiles: (NSMutableDictionary *) aDict { NSEnumerator *keyEnumerator; NSMutableDictionary *mappingDict, *aMutableDict; NSString *profileName; NSDictionary *sourceDict; BOOL foundGlobalProfile = NO; // recursively copy the dictionary to ensure mutability if(aDict != nil) { keyEnumerator = [aDict keyEnumerator]; while((profileName = [keyEnumerator nextObject]) != nil) { sourceDict = [aDict objectForKey: profileName]; mappingDict = [[NSMutableDictionary alloc] initWithDictionary: sourceDict]; // make sure key mapping dict is mutable if([sourceDict objectForKey: @"Key Mappings"] != nil) { aMutableDict = [[NSMutableDictionary alloc] initWithDictionary: [sourceDict objectForKey: @"Key Mappings"]]; [mappingDict setObject: aMutableDict forKey: @"Key Mappings"]; [aMutableDict release]; } [profiles setObject: mappingDict forKey: profileName]; [mappingDict release]; if([[sourceDict objectForKey: @"Global Profile"] isEqualToString: @"Yes"]) foundGlobalProfile = YES; } } if(foundGlobalProfile == NO) { mappingDict = [[NSMutableDictionary alloc] init]; [mappingDict setObject: @"Yes" forKey: @"Global Profile"]; [profiles setObject: mappingDict forKey: NSLocalizedStringFromTableInBundle(@"Global",@"iTerm", [NSBundle bundleForClass: [self class]], @"Key Binding Profiles")]; [mappingDict release]; } } - (BOOL) isGlobalProfile: (NSString *)profileName { NSDictionary *aProfile; if([profileName length] <= 0) return (NO); aProfile = [profiles objectForKey: profileName]; return ([[aProfile objectForKey: @"Global Profile"] isEqualToString: @"Yes"]); } - (void) addProfileWithName: (NSString *) aString copyProfile: (NSString *) profileName { //NSLog(@"%s: %@", __PRETTY_FUNCTION__, aString); if([aString length] > 0 && [profiles objectForKey: aString] == nil) { NSMutableDictionary *newProfile, *sourceProfile, *mappingDict; if([profileName length] > 0) { sourceProfile = [profiles objectForKey: profileName]; newProfile = [[NSMutableDictionary alloc] initWithDictionary: sourceProfile]; // make sure key mapping dict is mutable if([sourceProfile objectForKey: @"Key Mappings"] != nil) { mappingDict = [[NSMutableDictionary alloc] initWithDictionary: [sourceProfile objectForKey: @"Key Mappings"]]; [newProfile setObject: mappingDict forKey: @"Key Mappings"]; [mappingDict release]; } [newProfile removeObjectForKey: @"Global Profile"]; } else { newProfile = [[NSMutableDictionary alloc] init]; } [profiles setObject: newProfile forKey: aString]; [newProfile release]; } else NSBeep(); } - (void) deleteProfileWithName: (NSString *) aString { if([aString length] > 0) { [profiles removeObjectForKey: aString]; } } - (int) numberOfEntriesInProfile: (NSString *) profileName { NSDictionary *keyMappings; //NSLog(@"%s: %@", __PRETTY_FUNCTION__, profileName); if([profileName length] > 0) { keyMappings = [[profiles objectForKey: profileName] objectForKey: @"Key Mappings"]; return ([keyMappings count]); } else return (0); } - (NSString *) keyCombinationAtIndex: (int) index inProfile: (NSString *) profile { NSMutableDictionary *keyMappings; NSArray *allKeys; NSString *theKeyCombination, *aString; NSMutableString *theKeyString; unsigned int keyCode, keyModifiers; //NSLog(@"%s: %@", __PRETTY_FUNCTION__, profile); keyMappings = [[profiles objectForKey: profile] objectForKey: @"Key Mappings"]; allKeys = [keyMappings allKeys]; if(index >= 0 && index < [allKeys count]) { theKeyCombination = [allKeys objectAtIndex: index]; } else return (nil); keyCode = keyModifiers = 0; sscanf([theKeyCombination UTF8String], "%x-%x", &keyCode, &keyModifiers); switch (keyCode) { case NSDownArrowFunctionKey: aString = NSLocalizedStringFromTableInBundle(@"cursor down",@"iTerm", [NSBundle bundleForClass: [self class]], @"Key Names"); break; case NSLeftArrowFunctionKey: aString = NSLocalizedStringFromTableInBundle(@"cursor left",@"iTerm", [NSBundle bundleForClass: [self class]], @"Key Names"); break; case NSRightArrowFunctionKey: aString = NSLocalizedStringFromTableInBundle(@"cursor right",@"iTerm", [NSBundle bundleForClass: [self class]], @"Key Names"); break; case NSUpArrowFunctionKey: aString = NSLocalizedStringFromTableInBundle(@"cursor up",@"iTerm", [NSBundle bundleForClass: [self class]], @"Key Names"); break; case NSDeleteFunctionKey: aString = NSLocalizedStringFromTableInBundle(@"del",@"iTerm", [NSBundle bundleForClass: [self class]], @"Key Names"); break; case 0x7f: aString = NSLocalizedStringFromTableInBundle(@"delete",@"iTerm", [NSBundle bundleForClass: [self class]], @"Key Names"); break; case NSEndFunctionKey: aString = NSLocalizedStringFromTableInBundle(@"end",@"iTerm", [NSBundle bundleForClass: [self class]], @"Key Names"); break; case NSF1FunctionKey: case NSF2FunctionKey: case NSF3FunctionKey: case NSF4FunctionKey: case NSF5FunctionKey: case NSF6FunctionKey: case NSF7FunctionKey: case NSF8FunctionKey: case NSF9FunctionKey: case NSF10FunctionKey: case NSF11FunctionKey: case NSF12FunctionKey: case NSF13FunctionKey: case NSF14FunctionKey: case NSF15FunctionKey: case NSF16FunctionKey: case NSF17FunctionKey: case NSF18FunctionKey: case NSF19FunctionKey: case NSF20FunctionKey: aString = [NSString stringWithFormat: @"F%d", (keyCode - NSF1FunctionKey + 1)]; break; case NSHelpFunctionKey: aString = NSLocalizedStringFromTableInBundle(@"help",@"iTerm", [NSBundle bundleForClass: [self class]], @"Key Names"); break; case NSHomeFunctionKey: aString = NSLocalizedStringFromTableInBundle(@"home",@"iTerm", [NSBundle bundleForClass: [self class]], @"Key Names"); break; case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': aString = [NSString stringWithFormat: @"%d", (keyCode - '0')]; break; case '=': aString = @"="; break; case '/': aString = @"/"; break; case '*': aString = @"*"; break; case '-': aString = @"-"; break; case '+': aString = @"+"; break; case '.': aString = @"."; break; case NSClearLineFunctionKey: aString = NSLocalizedStringFromTableInBundle(@"numlock",@"iTerm", [NSBundle bundleForClass: [self class]], @"Key Names"); break; case NSPageDownFunctionKey: aString = NSLocalizedStringFromTableInBundle(@"page down",@"iTerm", [NSBundle bundleForClass: [self class]], @"Key Names"); break; case NSPageUpFunctionKey: aString = NSLocalizedStringFromTableInBundle(@"page up",@"iTerm", [NSBundle bundleForClass: [self class]], @"Key Names"); break; case 0x3: // 'enter' on numeric key pad aString = NSLocalizedStringFromTableInBundle(@"enter",@"iTerm", [NSBundle bundleForClass: [self class]], @"Key Names"); break; default: aString = [NSString stringWithFormat: @"%@ 0x%x", NSLocalizedStringFromTableInBundle(@"hex code",@"iTerm", [NSBundle bundleForClass: [self class]], @"Key Names"), keyCode]; break; } theKeyString = [[NSMutableString alloc] initWithString: @""]; if(keyModifiers & NSCommandKeyMask) { [theKeyString appendString: @"cmd-"]; } if(keyModifiers & NSAlternateKeyMask) { [theKeyString appendString: @"opt-"]; } if(keyModifiers & NSControlKeyMask) { [theKeyString appendString: @"ctrl-"]; } if(keyModifiers & NSShiftKeyMask) { [theKeyString appendString: @"shift-"]; } if(keyModifiers & NSNumericPadKeyMask) { [theKeyString appendString: @"num-"]; } [theKeyString appendString: aString]; return ([theKeyString autorelease]); } - (NSString *) actionForKeyCombinationAtIndex: (int) index inProfile: (NSString *) profile { NSMutableDictionary *keyMappings; NSArray *allKeys; int action; NSString *actionString; NSString *auxText; //NSLog(@"%s: %@", __PRETTY_FUNCTION__, profile); keyMappings = [[profiles objectForKey: profile] objectForKey: @"Key Mappings"]; allKeys = [keyMappings allKeys]; if(index >= 0 && index < [allKeys count]) { action = [[[keyMappings objectForKey: [allKeys objectAtIndex: index]] objectForKey: @"Action"] intValue]; auxText = [[keyMappings objectForKey: [allKeys objectAtIndex: index]] objectForKey: @"Text"]; } else return (nil); switch (action) { case KEY_ACTION_NEXT_SESSION: actionString = NSLocalizedStringFromTableInBundle(@"next tab",@"iTerm", [NSBundle bundleForClass: [self class]], @"Key Binding Actions"); break; case KEY_ACTION_NEXT_WINDOW: actionString = NSLocalizedStringFromTableInBundle(@"next window",@"iTerm", [NSBundle bundleForClass: [self class]], @"Key Binding Actions"); break; case KEY_ACTION_PREVIOUS_SESSION: actionString = NSLocalizedStringFromTableInBundle(@"previous tab",@"iTerm", [NSBundle bundleForClass: [self class]], @"Key Binding Actions"); break; case KEY_ACTION_PREVIOUS_WINDOW: actionString = NSLocalizedStringFromTableInBundle(@"previous window",@"iTerm", [NSBundle bundleForClass: [self class]], @"Key Binding Actions"); break; case KEY_ACTION_SCROLL_END: actionString = NSLocalizedStringFromTableInBundle(@"scroll end",@"iTerm", [NSBundle bundleForClass: [self class]], @"Key Binding Actions"); break; case KEY_ACTION_SCROLL_HOME: actionString = NSLocalizedStringFromTableInBundle(@"scroll home",@"iTerm", [NSBundle bundleForClass: [self class]], @"Key Binding Actions"); break; case KEY_ACTION_SCROLL_LINE_DOWN: actionString = NSLocalizedStringFromTableInBundle(@"scroll line down",@"iTerm", [NSBundle bundleForClass: [self class]], @"Key Binding Actions"); break; case KEY_ACTION_SCROLL_LINE_UP: actionString = NSLocalizedStringFromTableInBundle(@"scroll line up",@"iTerm", [NSBundle bundleForClass: [self class]], @"Key Binding Actions"); break; case KEY_ACTION_SCROLL_PAGE_DOWN: actionString = NSLocalizedStringFromTableInBundle(@"scroll page down",@"iTerm", [NSBundle bundleForClass: [self class]], @"Key Binding Actions"); break; case KEY_ACTION_SCROLL_PAGE_UP: actionString = NSLocalizedStringFromTableInBundle(@"scroll page up",@"iTerm", [NSBundle bundleForClass: [self class]], @"Key Binding Actions"); break; case KEY_ACTION_ESCAPE_SEQUENCE: actionString = [NSString stringWithFormat:@"%@ %@", NSLocalizedStringFromTableInBundle(@"send ^[",@"iTerm", [NSBundle bundleForClass: [self class]], @"Key Binding Actions"), auxText]; break; case KEY_ACTION_HEX_CODE: actionString = [NSString stringWithFormat: @"%@ %@", NSLocalizedStringFromTableInBundle(@"send hex code",@"iTerm", [NSBundle bundleForClass: [self class]], @"Key Binding Actions"), auxText]; break; case KEY_ACTION_IGNORE: actionString = NSLocalizedStringFromTableInBundle(@"ignore",@"iTerm", [NSBundle bundleForClass: [self class]], @"Key Binding Actions"); break; default: actionString = [NSString stringWithFormat: @"%@ %d", NSLocalizedStringFromTableInBundle(@"unknown action ID",@"iTerm", [NSBundle bundleForClass: [self class]], @"Key Binding Actions"), action]; break; } return (actionString); } - (void) addEntryForKeyCode: (unsigned int) hexCode modifiers: (unsigned int) modifiers action: (unsigned int) action text: (NSString *) text profile: (NSString *) profile { //NSLog(@"%s", __PRETTY_FUNCTION__); NSMutableDictionary *keyMappings, *keyBinding; NSString *keyString; if([profile length] <= 0) return; keyMappings = [[profiles objectForKey: profile] objectForKey: @"Key Mappings"]; if(keyMappings == nil) { keyMappings = [[NSMutableDictionary alloc] init]; [[profiles objectForKey: profile] setObject: keyMappings forKey: @"Key Mappings"]; [keyMappings release]; } keyString = [NSString stringWithFormat: @"0x%x-0x%x", hexCode, modifiers]; keyBinding = [[NSMutableDictionary alloc] init]; [keyBinding setObject: [NSNumber numberWithInt: action] forKey: @"Action"]; if([text length] > 0) [keyBinding setObject:[[text copy] autorelease] forKey: @"Text"]; [keyMappings setObject: keyBinding forKey: keyString]; [keyBinding release]; } - (void) addEntryForKey: (unsigned int) key modifiers: (unsigned int) modifiers action: (unsigned int) action text: (NSString *) text profile: (NSString *) profile { //NSLog(@"%s", __PRETTY_FUNCTION__); unsigned int keyModifiers; unichar keyUnicode; keyModifiers = modifiers; // this is how we distinguish between regular numbers and those on the numeric keypad if(key >= KEY_NUMERIC_0 && key <= KEY_NUMERIC_PERIOD) keyModifiers |= NSNumericPadKeyMask; // on some keyboards, arrow keys have NSNumericPadKeyMask bit set; manually set it for keyboards that don't if(key >= KEY_CURSOR_DOWN && key <= KEY_CURSOR_UP) keyModifiers |= NSNumericPadKeyMask; switch (key) { case KEY_CURSOR_DOWN: keyUnicode = NSDownArrowFunctionKey; break; case KEY_CURSOR_LEFT: keyUnicode = NSLeftArrowFunctionKey; break; case KEY_CURSOR_RIGHT: keyUnicode = NSRightArrowFunctionKey; break; case KEY_CURSOR_UP: keyUnicode = NSUpArrowFunctionKey; break; case KEY_DEL: keyUnicode = NSDeleteFunctionKey; break; case KEY_DELETE: keyUnicode = 0x7f; break; case KEY_END: keyUnicode = NSEndFunctionKey; break; case KEY_F1: case KEY_F2: case KEY_F3: case KEY_F4: case KEY_F5: case KEY_F6: case KEY_F7: case KEY_F8: case KEY_F9: case KEY_F10: case KEY_F11: case KEY_F12: case KEY_F13: case KEY_F14: case KEY_F15: case KEY_F16: case KEY_F17: case KEY_F18: case KEY_F19: case KEY_F20: keyUnicode = NSF1FunctionKey + (key - KEY_F1); break; case KEY_HELP: keyUnicode = NSHelpFunctionKey; break; case KEY_HOME: keyUnicode = NSHomeFunctionKey; break; case KEY_NUMERIC_0: case KEY_NUMERIC_1: case KEY_NUMERIC_2: case KEY_NUMERIC_3: case KEY_NUMERIC_4: case KEY_NUMERIC_5: case KEY_NUMERIC_6: case KEY_NUMERIC_7: case KEY_NUMERIC_8: case KEY_NUMERIC_9: keyUnicode = '0' + (key - KEY_NUMERIC_0); break; case KEY_NUMERIC_ENTER: keyUnicode = 0x3; // 'enter' on numeric keypad break; case KEY_NUMERIC_EQUAL: keyUnicode = '='; break; case KEY_NUMERIC_DIVIDE: keyUnicode = '/'; break; case KEY_NUMERIC_MULTIPLY: keyUnicode = '*'; break; case KEY_NUMERIC_MINUS: keyUnicode = '-'; break; case KEY_NUMERIC_PLUS: keyUnicode = '+'; break; case KEY_NUMERIC_PERIOD: keyUnicode = '.'; break; case KEY_NUMLOCK: keyUnicode = NSClearLineFunctionKey; break; case KEY_PAGE_DOWN: keyUnicode = NSPageDownFunctionKey; break; case KEY_PAGE_UP: keyUnicode = NSPageUpFunctionKey; break; default: NSLog(@"%s: unknown key %d", __PRETTY_FUNCTION__, key); return; } [self addEntryForKeyCode: keyUnicode modifiers: keyModifiers action: action text: text profile: profile]; } - (void) deleteEntryAtIndex: (int) index inProfile: (NSString *) profile { NSMutableDictionary *keyMappings; NSArray *allKeys; NSString *keyString; keyMappings = [[profiles objectForKey: profile] objectForKey: @"Key Mappings"]; allKeys = [keyMappings allKeys]; if(index >= 0 && index < [allKeys count]) { keyString = [allKeys objectAtIndex: index]; [keyMappings removeObjectForKey: keyString]; } } - (int) optionKeyForProfile: (NSString *) profileName { NSDictionary *aProfile; if([profileName length] > 0) aProfile = [profiles objectForKey: profileName]; else aProfile = [self globalProfile]; return ([[aProfile objectForKey: @"Option Key"] intValue]); } - (void) setOptionKey: (int) option forProfile: (NSString *) profileName { NSMutableDictionary *aProfile; //NSLog(@"%s: profile = %@; option = %d", __PRETTY_FUNCTION__, profileName, option); aProfile = [profiles objectForKey: profileName]; [aProfile setObject: [NSNumber numberWithInt: option] forKey: @"Option Key"]; } - (NSDictionary *) globalProfile { NSString *globalProfile; globalProfile = NSLocalizedStringFromTableInBundle(@"Global",@"iTerm", [NSBundle bundleForClass: [self class]], @"Key Binding Profiles"); return ([profiles objectForKey: globalProfile]); } - (NSString *) globalProfileName { NSDictionary *aProfile; NSEnumerator *keyEnumerator; NSString *aKey, *aProfileName; keyEnumerator = [profiles keyEnumerator]; aProfileName = nil; while ((aKey = [keyEnumerator nextObject])) { aProfile = [profiles objectForKey: aKey]; if([self isGlobalProfile: aKey]) { aProfileName = aKey; break; } } return (aProfileName); } - (int) actionForKeyCode: (unichar)keyCode modifiers: (unsigned int) keyModifiers text: (NSString **) text profile: (NSString *)profile { int retCode = -1; NSString *globalProfile; // search the specified profile first if([profile length] > 0) retCode = [self _actionForKeyCode: keyCode modifiers: keyModifiers text: text profile: profile]; // If we found something in the common profile, return that if(retCode >= 0) return (retCode); globalProfile = NSLocalizedStringFromTableInBundle(@"Global",@"iTerm", [NSBundle bundleForClass: [self class]], @"Key Binding Profiles"); // search the common profile if a match was not found if([globalProfile length] > 0) retCode = [self _actionForKeyCode: keyCode modifiers: keyModifiers text: text profile: globalProfile]; return (retCode); } @end @implementation iTermKeyBindingMgr (Private) - (int) _actionForKeyCode: (unichar)keyCode modifiers: (unsigned int) keyModifiers text: (NSString **) text profile: (NSString *)profile { NSDictionary *keyMappings; NSString *keyString; NSDictionary *theKeyMapping; int retCode = -1; unsigned int theModifiers; if(profile == nil) { if(text) *text = nil; return (-1); } keyMappings = [[profiles objectForKey: profile] objectForKey: @"Key Mappings"]; if(keyMappings == nil) { if(text) *text = nil; return (-1); } // turn off all the other modifier bits we don't care about theModifiers = keyModifiers & (NSAlternateKeyMask | NSControlKeyMask | NSShiftKeyMask | NSCommandKeyMask | NSNumericPadKeyMask); // on some keyboards, arrow keys have NSNumericPadKeyMask bit set; manually set it for keyboards that don't if(keyCode >= NSUpArrowFunctionKey && keyCode <= NSRightArrowFunctionKey) theModifiers |= NSNumericPadKeyMask; keyString = [NSString stringWithFormat: @"0x%x-0x%x", keyCode, theModifiers]; theKeyMapping = [keyMappings objectForKey: keyString]; if(theKeyMapping == nil) { if(text) *text = nil; return (-1); } // parse the mapping retCode = [[theKeyMapping objectForKey: @"Action"] intValue]; if(text != nil) *text = [theKeyMapping objectForKey: @"Text"]; return (retCode); } @end