diff --git a/Headers/gnustep/gui/NSInputManager.h b/Headers/gnustep/gui/NSInputManager.h index 85a6c94a0..41d7be0ea 100644 --- a/Headers/gnustep/gui/NSInputManager.h +++ b/Headers/gnustep/gui/NSInputManager.h @@ -1,13 +1,10 @@ /* -*-objc-*- NSInputManager.h - Copyright (C) 2001 Free Software Foundation, Inc. - - Author: Fred Kiefer - Date: August 2001 + Copyright (C) 2001, 2002 Free Software Foundation, Inc. Author: Nicola Pero - Date: December 2001 + Date: December 2001, February 2002 This file is part of the GNUstep GUI Library. @@ -57,37 +54,76 @@ - (void) insertText: (id)aString; @end -struct _GSInputManagerBinding -{ - /* The character this binding is about. */ - unichar character; - - /* The character is bound to different selectors according to the - various modifiers which can be used with the character. - - There are eight selectors for the eight possibilities - each of - NSShiftKeyMask, NSControlKeyMask, NSAlternateKeyMask might be on - or off and each combination gives a different selector. - - The index of the selector to use is given by - - (modifiers & (NSShiftKeyMask | NSControlKeyMask | NSAlternateKeyMask)) / 2 - - a NULL selector means use the default action */ - SEL selector[8]; -}; - +/* The input manager understands quite sophisticated keybindings. + * + * A certain keystroke (represented by a unichar + some modifiers) can + * be bound to a selector. For example: "Control-f" = "moveForward:"; + * If you press Control-f, the selector moveForward: is invoked. + * + * A certain keystroke can be bound to an array of selectors. For + * example: "Control-k" = ("moveToBeginningOfLine:", "deleteToEndOfLine:"); + * If you press Control-k, the selector moveToBeginningOfLine: is invoked, + * immediately followed by deleteToEndOfLine:. + * + * A certain keystroke can be bound to a dictionary of other + * keybindings. For example "Control-c" = { "Control-f" = + * "openFile:"; "Control-s" = "save:"; }; + * If you press Control-c followed by Control-f, openFile: is invoked; + * if you press Control-c followed by Control-s, save: is invoked. + * + * Any keystroke which is not bound by a keybinding is basically inserted + * as it is by calling 'insertText:' of the caller. + * + * Control-g is normally bound to aborting the current keybinding + * sequence. Whenever you are confused about what the hell you have + * typed and what strange command the input manager is going to + * understand, just type Control-g to discard past pending keystrokes, + * and reset the input manager. + * + * Control-q is normally bound to literally quoting the next + * keystroke. That is, the next keystroke is *not* interpreted by the + * input manager, but rather inserted literally into the text. + */ +@class GSKeyBindingTable; @interface NSInputManager: NSObject { + /* The current client we are working for. */ id _currentClient; - - /* An array of bindings. */ - struct _GSInputManagerBinding *_bindings; - /* The size of the array. */ - int _bindingsCount; + /* This is the basic, root set of bindings. Whenever the input + manager detects that the current client has changed, it immediately + resets the current key bindings to the root ones. If you are typing + and are confused about what's happening, pressing Control-g always + resets the bindings to the root bindings. */ + GSKeyBindingTable *_rootBindingTable; + + /* These are the bindings which will be used to interpret the next + keystroke. At the beginning, this is the same as the + _rootBindingTable. But when you type a keystroke which is the + beginning of a sequence of keystrokes producing a certain action, + then the input manager updates the _currentBindingTable to be the + table where he looks up the next keystroke you put in. + */ + GSKeyBindingTable *_currentBindingTable; + + /* When we are reading multi-keystroke bindings, we need to remember + the keystrokes we read thinking they were the beginning of a + multi-keystroke binding ... just in case it turns out that they + are not :-) */ + NSMutableArray *_pendingKeyEvents; + + /* When it is YES, the next key stroke is interpreted literally rather + than looked up using the _currentBindingTable. */ + BOOL _interpretNextKeyStrokeLiterally; + + /* Extremely special keybinding which overrides any other keybinding + in all contexts - abort - normally bound to Control-g. When we + encounter this keystroke, we abort all pending keystrokes and + reset ourselves immediately into vanilla root input state. */ + unichar _abortCharacter; + int _abortFlags; } + (NSInputManager *) currentInputManager; @@ -105,6 +141,41 @@ struct _GSInputManagerBinding - (BOOL) wantsToDelayTextChangeNotifications; - (BOOL) wantsToHandleMouseEvents; - (BOOL) wantsToInterpretAllKeystrokes; + +/* GNUstep Extensions. */ + +/* Parses a key as found in a keybinding file. + key is something like 'Control-f' or 'Control-Shift-LeftArrow'. + Returns YES if the key could be parsed, NO if not. If the key + could be parsed, character will contain the unichar, and modifiers + the modifiers. */ ++ (BOOL) parseKey: (NSString *)key + intoCharacter: (unichar *)character + andModifiers: (int *)modifiers; + +/* This is used to produce a key description which can be put into a + keybinding file from an actual keystroke. The gnustep-gui never + needs this :-) since it only reads keybinding files, never writes + them, but Preferences applications might need it - they can have + the user type in the desired keystroke, then call this method to + turn the keystroke into a string which can be put in keybindings + files. Pass 0 as modifiers if you only want the name of the + keystroke, ignoring modifiers. */ ++ (NSString *) describeKeyStroke: (unichar)character + withModifiers: (int)modifiers; + +/* Methods used internally ... not really part of the public API, can change + without notice. */ + +/* Reset the internal state. Normally bound to Control-g [regardless + of context!], but also automatically done whenever the current + client changes. */ +- (void) resetInternalState; + +/* Quote the next key stroke. Normally bound to Control-q. */ +- (void) quoteNextKeyStroke; + + @end #endif /* _GNUstep_H_NSInputManager */ diff --git a/Source/NSInputManager.m b/Source/NSInputManager.m index 94e81f8f1..3a19abb84 100644 --- a/Source/NSInputManager.m +++ b/Source/NSInputManager.m @@ -1,9 +1,9 @@ -/** NSInputManager -*-objc-*- +/* NSInputManager -*-objc-*- Copyright (C) 2001 Free Software Foundation, Inc. Author: Nicola Pero - Date: December 2001 + Date: December 2001, January 2002 This file is part of the GNUstep GUI Library. @@ -28,8 +28,11 @@ #include #include +#include "GSKeyBindingAction.h" +#include "GSKeyBindingTable.h" + /* A table mapping character names to characters, used to interpret - the character names found in KeyBindings dictionary files. */ + the character names found in KeyBindings dictionaries. */ #define CHARACTER_TABLE_SIZE 77 static struct @@ -124,7 +127,6 @@ character_table[CHARACTER_TABLE_SIZE] = static NSInputManager *currentInputManager = nil; - @implementation NSInputManager + (NSInputManager *) currentInputManager @@ -137,24 +139,20 @@ static NSInputManager *currentInputManager = nil; return currentInputManager; } -- (void) bindKey: (NSString *)key toAction: (NSString *)action ++ (BOOL) parseKey: (NSString *)key + intoCharacter: (unichar *)character + andModifiers: (int *)modifiers { - /* First we try to parse the key into a character/flags couple */ - unichar character = 0; - unsigned flags = 0; - BOOL isFunctionKey = NO; - /* Then we parse the action into a selector */ - SEL selector; - - NSString *c; + int flags = 0; + unichar c = 0; /* Parse the key: first break it into segments separated by - */ NSArray *components = [key componentsSeparatedByString: @"-"]; + NSString *name; /* Then, parse the modifiers. The modifiers are the components - all of them except the last one! */ int i, count = [components count]; - int index; for (i = 0; i < count - 1; i++) { @@ -179,122 +177,109 @@ static NSInputManager *currentInputManager = nil; { flags |= NSShiftKeyMask; } + else if ([modifier isEqualToString: @"NumericPad"] + || [modifier isEqualToString: @"Numeric"] + || [modifier isEqualToString: @"N"]) + { + flags |= NSNumericPadKeyMask; + } else { NSLog (@"NSInputManager - unknown modifier '%@' ignored", modifier); + return NO; } } - /* The actual index in the little SEL table is the following one. */ - index = flags / 2; - /* Now, parse the actual key. */ - c = [components objectAtIndex: (count - 1)]; + name = [components objectAtIndex: (count - 1)]; - if ([c isEqualToString: @""]) + if ([name isEqualToString: @""]) { /* This happens if '-' was the character. */ - character = '-'; + c = '-'; } - else if ([c length] == 1) + else if ([name length] == 1) { /* A single character, such as 'a'. */ - character = [c characterAtIndex: 0]; + c = [name characterAtIndex: 0]; } else { /* A descriptive string, such as Tab or Home. */ for (i = 0; i < CHARACTER_TABLE_SIZE; i++) { - if ([c isEqualToString: (character_table[i]).name]) + if ([name isEqualToString: (character_table[i]).name]) { - character = (character_table[i]).character; - isFunctionKey = YES; + c = (character_table[i]).character; + flags |= NSFunctionKeyMask; break; } } if (i == CHARACTER_TABLE_SIZE) { NSLog (@"NSInputManager - unknown character '%@' ignored", c); - return; + return NO; + } + } + if (character != NULL) + { + *character = c; + } + + if (modifiers != NULL) + { + *modifiers = flags; + } + + return YES; +} + ++ (NSString *) describeKeyStroke: (unichar)character + withModifiers: (int)modifiers +{ + NSMutableString *description = [NSMutableString new]; + int i; + + if (modifiers & NSCommandKeyMask) + { + [description appendString: @"Command-"]; + } + + if (modifiers & NSControlKeyMask) + { + [description appendString: @"Control-"]; + } + + if (modifiers & NSAlternateKeyMask) + { + [description appendString: @"Alternate-"]; + } + + if (modifiers & NSShiftKeyMask) + { + [description appendString: @"Shift-"]; + } + + if (modifiers & NSNumericPadKeyMask) + { + [description appendString: @"Numeric-"]; + } + + for (i = 0; i < CHARACTER_TABLE_SIZE; i++) + { + if (character == ((character_table[i]).character)) + { + [description appendString: character_table[i].name]; + break; } } - /* "" as action means disable the keybinding. It can be used to override - a previous keybinding. */ - if ([action isEqualToString: @""]) + if (i == CHARACTER_TABLE_SIZE) { - selector = NULL; + NSString *c = [NSString stringWithCharacters: &character length: 1]; + [description appendString: c]; } - else - { - selector = NSSelectorFromString (action); - if (selector == NULL) - { - NSLog (@"NSInputManager: unknown selector '%@' ignored", action); - return; - } - } - - /* Check if there are already some bindings for this character. */ - for (i = 0; i < _bindingsCount; i++) - { - if (_bindings[i].character == character) - { - (_bindings[i]).selector[index] = selector; - if (!isFunctionKey) - { - /* Not a function key - set the selector for the character - with shift as well - we don't actually know if the character - is typed with shift or not ! */ - flags |= NSShiftKeyMask; - index = flags / 2; - - (_bindings[i]).selector[index] = selector; - } - return; - } - } - - /* Ok - allocate memory for the new binding. */ - if (_bindingsCount == 0) - { - _bindingsCount = 1; - _bindings = objc_malloc (sizeof (struct _GSInputManagerBinding)); - } - else - { - _bindingsCount++; - _bindings = objc_realloc (_bindings, - sizeof (struct _GSInputManagerBinding) - * _bindingsCount); - } - _bindings[_bindingsCount - 1].character = character; - - /* Set to NULL all selectors. */ - for (i = 0; i < 8; i++) - { - (_bindings[_bindingsCount - 1]).selector[i] = NULL; - } - - /* Now save the selector. */ - (_bindings[_bindingsCount - 1]).selector[index] = selector; - if (!isFunctionKey) - { - /* Not a function key - set the selector for the character - with shift as well - we don't actually know if the character - is typed with shift or not ! */ - flags |= NSShiftKeyMask; - index = flags / 2; - - (_bindings[_bindingsCount - 1]).selector[index] = selector; - } -} - -- (void) dealloc -{ - objc_free (_bindings); - return; + return description; } - (void) loadBindingsFromFile: (NSString *)fullPath @@ -302,8 +287,6 @@ static NSInputManager *currentInputManager = nil; NS_DURING { NSDictionary *bindings; - NSEnumerator *e; - NSString *key; bindings = [NSDictionary dictionaryWithContentsOfFile: fullPath]; if (bindings == nil) @@ -311,12 +294,7 @@ static NSInputManager *currentInputManager = nil; [NSException raise]; } - e = [bindings keyEnumerator]; - while ((key = [e nextObject]) != nil) - { - [self bindKey: key - toAction: [bindings objectForKey: key]]; - } + [_rootBindingTable loadBindingsFromDictionary: bindings]; } NS_HANDLER { @@ -363,6 +341,46 @@ static NSInputManager *currentInputManager = nil; self = [super init]; + _rootBindingTable = [GSKeyBindingTable new]; + + /* Read the abort key from the user defaults. */ + { + NSString *abortKey = [defaults stringForKey: @"GSAbortKey"]; + + if (abortKey == nil) + { + _abortCharacter = 'g'; + _abortFlags = NSControlKeyMask; + } + else if (![NSInputManager parseKey: abortKey + intoCharacter: &_abortCharacter + andModifiers: &_abortFlags]) + { + NSLog (@"Could not parse GSAbortKey - using Control-g"); + _abortCharacter = 'g'; + _abortFlags = NSControlKeyMask; + } + } + + /* Read the quote key from the user defaults. */ + { + NSString *quoteKey = [defaults stringForKey: @"GSQuoteKey"]; + GSKeyBindingActionQuoteNextKeyStroke *quoteAction; + + quoteAction = [[GSKeyBindingActionQuoteNextKeyStroke alloc] init]; + + if (quoteKey == nil) + { + quoteKey = @"Control-q"; + } + + [_rootBindingTable bindKey: quoteKey toAction: quoteAction]; + RELEASE (quoteAction); + } + + + /* FIXME all the following is gonna change. */ + /* Normally, when we start up, we load all the keybindings we find in the following files, in this order: @@ -436,6 +454,13 @@ static NSInputManager *currentInputManager = nil; NSEvent *theEvent; NSEnumerator *eventEnum = [eventArray objectEnumerator]; + /* If the client has changed, reset our internal state before going + on. */ + if (client != _currentClient) + { + [self resetInternalState]; + } + _currentClient = client; while ((theEvent = [eventEnum nextObject]) != nil) @@ -446,73 +471,151 @@ static NSInputManager *currentInputManager = nil; unsigned flags = [theEvent modifierFlags] & (NSShiftKeyMask | NSAlternateKeyMask | NSControlKeyMask); - BOOL done = NO; - int i; - + if ([unmodifiedCharacters length] > 0) { character = [unmodifiedCharacters characterAtIndex: 0]; } - - /* Look up the character in the keybindings dictionary. */ - for (i = 0; i < _bindingsCount; i++) + if (!_interpretNextKeyStrokeLiterally) { - if (_bindings[i].character == character) + GSKeyBindingAction *action; + GSKeyBindingTable *table; + BOOL found; + + /* Special keybinding recognized in all contexts - abort - + normally bound to Control-g. The user is confused and + wants to go home. Abort whatever train of thoughts we + were following, discarding whatever pending keystrokes we + have, and return into default state. */ + if (character == _abortCharacter && flags == _abortFlags) { - SEL selector = (_bindings[i]).selector[flags / 2]; + [self resetInternalState]; + break; + } + + /* Look up the character in the current keybindings table. */ + found = [_currentBindingTable lookupKeyStroke: character + modifiers: flags + returningActionIn: &action + tableIn: &table]; + + if (found) + { + if (action != nil) + { + /* First reset our internal state - we are done + interpreting this keystroke sequence. */ + [self resetInternalState]; + + /* Then perform the action. The action might actually + modify our internal state, which is why we reset it + before calling the action! (for example, performing + the action might cause us to interpret the next + keystroke literally). */ + [action performActionWithInputManager: self]; + break; + } + else if (table != nil) + { + /* It's part of a composite multi-stroke + keybinding. */ + _currentBindingTable = table; + [_pendingKeyEvents addObject: theEvent]; + break; + } + /* Else it is as if we didn't find it! */ + } + + /* Ok - the keybinding wasn't found. If we were tracking a + multi-stroke keybinding, it means we were on a false + track. */ + if ([_pendingKeyEvents count] > 0) + { + NSEvent *e; + + /* Save the pending events locally in this stack + frame. */ + NSMutableArray *a = _pendingKeyEvents; + RETAIN (a); - if (selector == NULL) - { - /* Get out of loop with done = NO - we found the - keybinding, but it was bound to the default - action, so we stop searching and fall back on the - default action. */ - break; - } - else - { - [self doCommandBySelector: selector]; - done = YES; - break; - } - } - } + /* Reset our internal state. */ + [self resetInternalState]; + + /* Take the very first event we received and which we + tried to interpret as a key binding, which now we + know was the wrong thing to do. */ + e = [a objectAtIndex: 0]; - /* If not yet found, perform the default action. */ - if (!done) + /* Interpret it literally, since interpreting it as a + keybinding failed. */ + _interpretNextKeyStrokeLiterally = YES; + [self handleKeyboardEvents: [NSArray arrayWithObject: e] + client: client]; + + /* Now feed the remaining pending key events to + ourselves for interpretation - again from + scratch. */ + [a removeObjectAtIndex: 0]; + [a addObject: theEvent]; + + [self handleKeyboardEvents: a + client: client]; + + RELEASE (a); + break; + } + } + + /* We couldn't (or shouldn't) find the keybinding ... perform + the default action - literally interpreting the + keystroke. */ + + /* If this was a forced literal interpretation, make sure the + next one is interpreted normally. */ + _interpretNextKeyStrokeLiterally = NO; + + switch (character) { - switch (character) + case NSBackspaceCharacter: + [self doCommandBySelector: @selector (deleteBackward:)]; + break; + + case NSTabCharacter: + if (flags & NSShiftKeyMask) { - case NSBackspaceCharacter: - [self doCommandBySelector: @selector (deleteBackward:)]; - break; - - case NSTabCharacter: - if (flags & NSShiftKeyMask) - { - [self doCommandBySelector: @selector (insertBacktab:)]; - } - else - { - [self doCommandBySelector: @selector (insertTab:)]; - } - break; - - case NSEnterCharacter: - case NSFormFeedCharacter: - case NSCarriageReturnCharacter: - [self doCommandBySelector: @selector (insertNewline:)]; - break; - - default: - [self insertText: characters]; - break; + [self doCommandBySelector: @selector (insertBacktab:)]; } + else + { + [self doCommandBySelector: @selector (insertTab:)]; + } + break; + + case NSEnterCharacter: + case NSFormFeedCharacter: + case NSCarriageReturnCharacter: + [self doCommandBySelector: @selector (insertNewline:)]; + break; + + default: + [self insertText: characters]; + break; } - } + } } +- (void) resetInternalState +{ + _currentBindingTable = _rootBindingTable; + ASSIGN (_pendingKeyEvents, [NSMutableArray array]); + _interpretNextKeyStrokeLiterally = NO; +} + +- (void) quoteNextKeyStroke +{ + _interpretNextKeyStrokeLiterally = YES; +} - (BOOL) handleMouseEvent: (NSEvent *)theMouseEvent {