From 6ea38de33417b6c4a88244772ec0e4b5213569b0 Mon Sep 17 00:00:00 2001 From: Sebastien Vincent Date: Tue, 29 Nov 2011 10:37:36 +0000 Subject: [PATCH] Adds global shortcut support. --- build.xml | 13 +- lib/felix.client.run.properties | 1 + lib/native/linux-64/libglobalshortcut.so | Bin 0 -> 36264 bytes lib/native/linux/libglobalshortcut.so | Bin 0 -> 31330 bytes lib/native/mac/libglobalshortcut.jnilib | Bin 0 -> 105304 bytes lib/native/windows-64/globalshortcut.dll | Bin 0 -> 81408 bytes lib/native/windows/globalshortcut.dll | Bin 0 -> 57344 bytes .../defaultkeybindings/keybindings-global | Bin 0 -> 360 bytes resources/languages/resources.properties | 6 + src/native/build.xml | 86 ++ src/native/globalshortcut/DDHotKeyCenter.h | 86 ++ src/native/globalshortcut/DDHotKeyCenter.m | 313 +++++ src/native/globalshortcut/javakey.h | 163 +++ ..._impl_globalshortcut_NativeKeyboardHook.cc | 924 +++++++++++++++ ...impl_globalshortcut_NativeKeyboardHook.cpp | 1019 +++++++++++++++++ ...r_impl_globalshortcut_NativeKeyboardHook.h | 61 + ...r_impl_globalshortcut_NativeKeyboardHook.m | 940 +++++++++++++++ .../HashtableConfigurationStore.java | 1 + .../impl/globalshortcut/CallShortcut.java | 195 ++++ .../GlobalShortcutActivator.java | 241 ++++ .../GlobalShortcutServiceImpl.java | 358 ++++++ .../globalshortcut/NativeKeyboardHook.java | 171 +++ .../NativeKeyboardHookDelegate.java | 44 + .../impl/globalshortcut/UIShortcut.java | 68 ++ .../globalshortcut/globalshortcut.manifest.mf | 13 + .../communicator/impl/gui/GuiActivator.java | 22 +- .../communicator/impl/gui/UIServiceImpl.java | 2 +- .../impl/gui/main/call/CallManager.java | 16 +- .../impl/gui/main/call/PreCallDialog.java | 4 +- .../gui/main/contactlist/ContactList.java | 11 +- .../gui/main/contactlist/ContactListPane.java | 11 +- .../keybindings/GlobalKeybindingSetImpl.java | 53 + .../keybindings/KeybindingsActivator.java | 34 + .../keybindings/KeybindingsServiceImpl.java | 143 +++ .../impl/keybindings/keybindings.manifest.mf | 1 + .../KeybindingChooserActivator.java | 83 +- .../KeybindingsConfigPanel.java | 35 +- .../chooser/BindingAdaptor.java | 20 +- .../chooser/BindingChooser.java | 56 +- .../chooser/BindingEntry.java | 14 +- .../chooser/BindingPanel.java | 42 +- .../GlobalShortcutConfigForm.java | 311 +++++ .../globalchooser/GlobalShortcutEntry.java | 248 ++++ .../GlobalShortcutTableModel.java | 199 ++++ .../globalchooser/Resources.java | 43 + .../keybindingChooser.manifest.mf | 5 +- .../globalshortcut/GlobalShortcutEvent.java | 42 + .../GlobalShortcutListener.java | 22 + .../globalshortcut/GlobalShortcutService.java | 41 + .../keybindings/GlobalKeybindingSet.java | 33 + .../service/keybindings/KeybindingSet.java | 14 +- .../keybindings/KeybindingsService.java | 23 + 52 files changed, 6139 insertions(+), 92 deletions(-) create mode 100755 lib/native/linux-64/libglobalshortcut.so create mode 100755 lib/native/linux/libglobalshortcut.so create mode 100755 lib/native/mac/libglobalshortcut.jnilib create mode 100644 lib/native/windows-64/globalshortcut.dll create mode 100644 lib/native/windows/globalshortcut.dll create mode 100644 resources/config/defaultkeybindings/keybindings-global create mode 100644 src/native/globalshortcut/DDHotKeyCenter.h create mode 100644 src/native/globalshortcut/DDHotKeyCenter.m create mode 100644 src/native/globalshortcut/javakey.h create mode 100644 src/native/globalshortcut/net_java_sip_communicator_impl_globalshortcut_NativeKeyboardHook.cc create mode 100644 src/native/globalshortcut/net_java_sip_communicator_impl_globalshortcut_NativeKeyboardHook.cpp create mode 100644 src/native/globalshortcut/net_java_sip_communicator_impl_globalshortcut_NativeKeyboardHook.h create mode 100644 src/native/globalshortcut/net_java_sip_communicator_impl_globalshortcut_NativeKeyboardHook.m create mode 100644 src/net/java/sip/communicator/impl/globalshortcut/CallShortcut.java create mode 100644 src/net/java/sip/communicator/impl/globalshortcut/GlobalShortcutActivator.java create mode 100644 src/net/java/sip/communicator/impl/globalshortcut/GlobalShortcutServiceImpl.java create mode 100644 src/net/java/sip/communicator/impl/globalshortcut/NativeKeyboardHook.java create mode 100644 src/net/java/sip/communicator/impl/globalshortcut/NativeKeyboardHookDelegate.java create mode 100644 src/net/java/sip/communicator/impl/globalshortcut/UIShortcut.java create mode 100644 src/net/java/sip/communicator/impl/globalshortcut/globalshortcut.manifest.mf create mode 100644 src/net/java/sip/communicator/impl/keybindings/GlobalKeybindingSetImpl.java create mode 100644 src/net/java/sip/communicator/plugin/keybindingchooser/globalchooser/GlobalShortcutConfigForm.java create mode 100644 src/net/java/sip/communicator/plugin/keybindingchooser/globalchooser/GlobalShortcutEntry.java create mode 100644 src/net/java/sip/communicator/plugin/keybindingchooser/globalchooser/GlobalShortcutTableModel.java create mode 100644 src/net/java/sip/communicator/plugin/keybindingchooser/globalchooser/Resources.java create mode 100644 src/net/java/sip/communicator/service/globalshortcut/GlobalShortcutEvent.java create mode 100644 src/net/java/sip/communicator/service/globalshortcut/GlobalShortcutListener.java create mode 100644 src/net/java/sip/communicator/service/globalshortcut/GlobalShortcutService.java create mode 100644 src/net/java/sip/communicator/service/keybindings/GlobalKeybindingSet.java diff --git a/build.xml b/build.xml index f32001959..ff1ea24c3 100644 --- a/build.xml +++ b/build.xml @@ -932,7 +932,8 @@ bundle-plugin-loggingutils,bundle-plugin-dnsconfig, bundle-provdisc,bundle-provdisc-dhcp,bundle-provdisc-mdns, bundle-provisioning,bundle-addrbook,bundle-plugin-ldap, - bundle-plugin-contactsourceconfig,bundle-plugin-certconfig"/> + bundle-plugin-contactsourceconfig,bundle-plugin-certconfig, + bundle-globalshortcut"/> @@ -2753,4 +2754,14 @@ javax.swing.event, javax.swing.border"/> prefix="net/java/sip/communicator/plugin/certconfig" /> + + + + + + + diff --git a/lib/felix.client.run.properties b/lib/felix.client.run.properties index a4bac2289..e5f98879e 100644 --- a/lib/felix.client.run.properties +++ b/lib/felix.client.run.properties @@ -120,6 +120,7 @@ felix.auto.start.66= \ reference:file:sc-bundles/replacement.jar felix.auto.start.67= \ + reference:file:sc-bundles/globalshortcut.jar \ reference:file:sc-bundles/pluginmanager.jar \ reference:file:sc-bundles/skinmanager.jar \ reference:file:sc-bundles/icqaccregwizz.jar \ diff --git a/lib/native/linux-64/libglobalshortcut.so b/lib/native/linux-64/libglobalshortcut.so new file mode 100755 index 0000000000000000000000000000000000000000..66c09da74708cdda281620610b35911b015911c4 GIT binary patch literal 36264 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�&U!)}#&|b(# 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 X@CQ;teZKD>4?ENK)T&76$3FfC8loV% literal 0 HcmV?d00001 diff --git a/lib/native/mac/libglobalshortcut.jnilib b/lib/native/mac/libglobalshortcut.jnilib new file mode 100755 index 0000000000000000000000000000000000000000..08e53243d8e81c49d62d0af8be38d0138bc8d226 GIT binary patch literal 105304 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 PX*a#^e}4ae%Ypv`u;X1Q literal 0 HcmV?d00001 diff --git a/lib/native/windows/globalshortcut.dll b/lib/native/windows/globalshortcut.dll new file mode 100644 index 0000000000000000000000000000000000000000..3b6e4d8e0dd858710ed9b99d3ce9e46b9f2fde30 GIT binary patch literal 57344 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 literal 0 HcmV?d00001 diff --git a/resources/languages/resources.properties b/resources/languages/resources.properties index 2b18b09d7..dffa5525f 100644 --- a/resources/languages/resources.properties +++ b/resources/languages/resources.properties @@ -1067,6 +1067,12 @@ plugin.keybindings.MAIN_PREVIOUS_TAB=Previous tab plugin.keybindings.MAIN_RENAME=Rename contact plugin.keybindings.OPEN_HISTORY=Show History plugin.keybindings.OPEN_SMILIES=Show Smileys +plugin.keybindings.globalchooser.ANSWER_CALL=Answer call +plugin.keybindings.globalchooser.HANGUP_CALL=Hangup call +plugin.keybindings.globalchooser.SHOW_CONTACTLIST=Show contact list +plugin.keybindings.globalchooser.SHORTCUT_NAME=Name +plugin.keybindings.globalchooser.SHORTCUT_PRIMARY=Primary shortcut +plugin.keybindings.globalchooser.SHORTCUT_SECOND=Second shortcut plugin.keybindings.PLUGIN_NAME=Keybindings # Notification Configuration Form diff --git a/src/native/build.xml b/src/native/build.xml index 42b94a9e5..3cdfac51a 100644 --- a/src/native/build.xml +++ b/src/native/build.xml @@ -895,6 +895,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/native/globalshortcut/DDHotKeyCenter.h b/src/native/globalshortcut/DDHotKeyCenter.h new file mode 100644 index 000000000..6274b3623 --- /dev/null +++ b/src/native/globalshortcut/DDHotKeyCenter.h @@ -0,0 +1,86 @@ +/* + DDHotKey -- DDHotKeyCenter.h + + Copyright (c) 2010, Dave DeLong + + Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. + + The software is provided "as is", without warranty of any kind, including all implied warranties of merchantability and fitness. In no event shall the author(s) or copyright holder(s) be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the software or the use or other dealings in the software. + */ + +#import + +#if NS_BLOCKS_AVAILABLE +//a convenient typedef for the required signature of a hotkey block callback +typedef void (^DDHotKeyTask)(NSEvent*); +#endif + +@interface DDHotKey : NSObject + +@property (nonatomic, readonly, retain) id target; +@property (nonatomic, readonly) SEL action; +@property (nonatomic, readonly, retain) id object; +#if NS_BLOCKS_AVAILABLE +@property (nonatomic, readonly, copy) DDHotKeyTask task; +#endif + +@property (nonatomic, readonly) unsigned short keyCode; +@property (nonatomic, readonly) NSUInteger modifierFlags; + +@end + +#pragma mark - + +@interface DDHotKeyCenter : NSObject { + +} + +/** + Register a target/action hotkey. + The modifierFlags must be a bitwise OR of NSCommandKeyMask, NSAlternateKeyMask, NSControlKeyMask, or NSShiftKeyMask; + Returns YES if the hotkey was registered; NO otherwise. + */ +- (BOOL) registerHotKeyWithKeyCode:(unsigned short)keyCode modifierFlags:(NSUInteger)flags target:(id)target action:(SEL)action object:(id)object; + +#if NS_BLOCKS_AVAILABLE +/** + Register a block callback hotkey. + The modifierFlags must be a bitwise OR of NSCommandKeyMask, NSAlternateKeyMask, NSControlKeyMask, or NSShiftKeyMask; + Returns YES if the hotkey was registered; NO otherwise. + */ +- (BOOL) registerHotKeyWithKeyCode:(unsigned short)keyCode modifierFlags:(NSUInteger)flags task:(DDHotKeyTask)task; +#endif + +/** + See if a hotkey exists with the specified keycode and modifier flags. + NOTE: this will only check among hotkeys you have explicitly registered with DDHotKeyCenter. This does not check all globally registered hotkeys. + */ +- (BOOL) hasRegisteredHotKeyWithKeyCode:(unsigned short)keyCode modifierFlags:(NSUInteger)flags; + +/** + Unregister a specific hotkey + */ +- (void) unregisterHotKey:(DDHotKey *)hotKey; + +/** + Unregister all hotkeys with a specific target + */ +- (void) unregisterHotKeysWithTarget:(id)target; + +/** + Unregister all hotkeys with a specific target and action + */ +- (void) unregisterHotKeysWithTarget:(id)target action:(SEL)action; + +/** + Unregister a hotkey with a specific keycode and modifier flags + */ +- (void) unregisterHotKeyWithKeyCode:(unsigned short)keyCode modifierFlags:(NSUInteger)flags; + +/** + Returns a set of currently registered hotkeys + **/ +- (NSSet *) registeredHotKeys; + +@end + diff --git a/src/native/globalshortcut/DDHotKeyCenter.m b/src/native/globalshortcut/DDHotKeyCenter.m new file mode 100644 index 000000000..241399915 --- /dev/null +++ b/src/native/globalshortcut/DDHotKeyCenter.m @@ -0,0 +1,313 @@ +/* + DDHotKey -- DDHotKeyCenter.m + + Copyright (c) 2010, Dave DeLong + + Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. + + The software is provided "as is", without warranty of any kind, including all implied warranties of merchantability and fitness. In no event shall the author(s) or copyright holder(s) be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the software or the use or other dealings in the software. + */ + +#import "DDHotKeyCenter.h" +#import +#import + +#pragma mark Private Global Declarations + +static NSMutableSet * _registeredHotKeys = nil; +static UInt32 _nextHotKeyID = 1; +OSStatus dd_hotKeyHandler(EventHandlerCallRef nextHandler, EventRef theEvent, void * userData); +UInt32 dd_translateModifierFlags(NSUInteger flags); +NSString* dd_stringifyModifierFlags(NSUInteger flags); + +#pragma mark DDHotKey + +@implementation DDHotKey + +- (id) target { return nil; } +- (SEL) action { return nil; } +- (id) object { return nil; } +- (unsigned short) keyCode { return 0; } +- (NSUInteger) modifierFlags { return 0; } + +#if NS_BLOCKS_AVAILABLE +- (DDHotKeyTask) task { return nil; } +#endif + +- (NSUInteger) hash { + return [self keyCode] + [self modifierFlags]; +} + +- (BOOL) isEqual:(id)object { + BOOL equal = NO; + if ([object isKindOfClass:[DDHotKey class]]) { + equal = ([object keyCode] == [self keyCode]); + equal &= ([object modifierFlags] == [self modifierFlags]); + } + return equal; +} + +- (NSString *) description { + NSString * flags = dd_stringifyModifierFlags([self modifierFlags]); + NSString * invokes = @"(block)"; + if ([self target] != nil && [self action] != nil) { + invokes = [NSString stringWithFormat:@"[%@ %@]", [self target], NSStringFromSelector([self action])]; + } + return [NSString stringWithFormat:@"%@\n\t(key: %hu\n\tflags: %@\n\tinvokes: %@)", [super description], [self keyCode], flags, invokes]; +} + +@end + +@interface _DDHotKey : DDHotKey { + @private + id target; + SEL action; + id object; + +#if NS_BLOCKS_AVAILABLE + DDHotKeyTask task; +#endif + + unsigned short keyCode; + NSUInteger modifierFlags; + UInt32 hotKeyID; + NSValue * hotKeyRef; +} + +@property (nonatomic, retain) id target; +@property (nonatomic) SEL action; +@property (nonatomic, retain) id object; +@property (nonatomic) unsigned short keyCode; +@property (nonatomic) NSUInteger modifierFlags; +@property (nonatomic) UInt32 hotKeyID; +@property (nonatomic, retain) NSValue * hotKeyRef; + +#if NS_BLOCKS_AVAILABLE +@property (nonatomic, copy) DDHotKeyTask task; +#endif + +- (void) invokeWithEvent:(NSEvent *)event; +- (BOOL) registerHotKey; +- (void) unregisterHotKey; + +@end + +@implementation _DDHotKey + +@synthesize target, action, object, keyCode, modifierFlags, hotKeyID, hotKeyRef; +#if NS_BLOCKS_AVAILABLE +@synthesize task; +#endif + +- (Class) class { return [DDHotKey class]; } + +- (void) invokeWithEvent:(NSEvent *)event { + if (target != nil && action != nil && [target respondsToSelector:action]) { + [target performSelector:action withObject:event withObject:object]; + } +#if NS_BLOCKS_AVAILABLE + else if (task != nil) { + task(event); + } +#endif +} + +- (NSString *) actionString { + return NSStringFromSelector(action); +} + +- (BOOL) registerHotKey { + EventHotKeyID keyID; + keyID.signature = 'htk1'; + keyID.id = _nextHotKeyID; + + EventHotKeyRef carbonHotKey; + UInt32 flags = dd_translateModifierFlags(modifierFlags); + OSStatus err = RegisterEventHotKey(keyCode, flags, keyID, GetEventDispatcherTarget(), 0, &carbonHotKey); + + //error registering hot key + if (err != 0) { return NO; } + + NSValue * refValue = [NSValue valueWithPointer:carbonHotKey]; + [self setHotKeyRef:refValue]; + [self setHotKeyID:_nextHotKeyID]; + + _nextHotKeyID++; + + return YES; +} + +- (void) unregisterHotKey { + EventHotKeyRef carbonHotKey = (EventHotKeyRef)[hotKeyRef pointerValue]; + UnregisterEventHotKey(carbonHotKey); + [self setHotKeyRef:nil]; +} + +- (void) dealloc { + [target release], target = nil; + [object release], object = nil; + if (hotKeyRef != nil) { + [self unregisterHotKey]; + [hotKeyRef release], hotKeyRef = nil; + } + [super dealloc]; +} + +@end + +#pragma mark DDHotKeyCenter + +@implementation DDHotKeyCenter + ++ (void) initialize { + if (self == [DDHotKeyCenter class] && _registeredHotKeys == nil) { + _registeredHotKeys = [[NSMutableSet alloc] init]; + _nextHotKeyID = 1; + EventTypeSpec eventSpec; + eventSpec.eventClass = kEventClassKeyboard; + eventSpec.eventKind = kEventHotKeyReleased; + InstallApplicationEventHandler(&dd_hotKeyHandler, 1, &eventSpec, NULL, NULL); + } +} + +- (NSSet *) hotKeysMatchingPredicate:(NSPredicate *)predicate { + return [_registeredHotKeys filteredSetUsingPredicate:predicate]; +} + +- (BOOL) hasRegisteredHotKeyWithKeyCode:(unsigned short)keyCode modifierFlags:(NSUInteger)flags { + NSPredicate * predicate = [NSPredicate predicateWithFormat:@"keyCode = %hu AND modifierFlags = %lu", keyCode, flags]; + return ([[self hotKeysMatchingPredicate:predicate] count] > 0); +} + +#if NS_BLOCKS_AVAILABLE +- (BOOL) registerHotKeyWithKeyCode:(unsigned short)keyCode modifierFlags:(NSUInteger)flags task:(DDHotKeyTask)task { + //we can't add a new hotkey if something already has this combo + if ([self hasRegisteredHotKeyWithKeyCode:keyCode modifierFlags:flags]) { return NO; } + + _DDHotKey * newHotKey = [[_DDHotKey alloc] init]; + [newHotKey setTask:task]; + [newHotKey setKeyCode:keyCode]; + [newHotKey setModifierFlags:flags]; + + BOOL success = [newHotKey registerHotKey]; + if (success) { + [_registeredHotKeys addObject:newHotKey]; + } + + [newHotKey release]; + return success; +} +#endif + +- (BOOL) registerHotKeyWithKeyCode:(unsigned short)keyCode modifierFlags:(NSUInteger)flags target:(id)target action:(SEL)action object:(id)object { + //we can't add a new hotkey if something already has this combo + if ([self hasRegisteredHotKeyWithKeyCode:keyCode modifierFlags:flags]) { return NO; } + + //build the hotkey object: + _DDHotKey * newHotKey = [[_DDHotKey alloc] init]; + [newHotKey setTarget:target]; + [newHotKey setAction:action]; + [newHotKey setObject:object]; + [newHotKey setKeyCode:keyCode]; + [newHotKey setModifierFlags:flags]; + + BOOL success = [newHotKey registerHotKey]; + if (success) { + [_registeredHotKeys addObject:newHotKey]; + } + + [newHotKey release]; + return success; +} + +- (void) unregisterHotKeysMatchingPredicate:(NSPredicate *)predicate { + //explicitly unregister the hotkey, since relying on the unregistration in -dealloc can be problematic + NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; + NSSet * matches = [self hotKeysMatchingPredicate:predicate]; + [_registeredHotKeys minusSet:matches]; + [matches makeObjectsPerformSelector:@selector(unregisterHotKey)]; + [pool release]; +} + +- (void) unregisterHotKey:(DDHotKey *)hotKey { + if (object_getClass(hotKey) == [_DDHotKey class]) { + _DDHotKey * key = (_DDHotKey *)hotKey; + [_registeredHotKeys removeObject:key]; + [key unregisterHotKey]; + } else { + [NSException raise:NSInvalidArgumentException format:@"Invalid hotkey"]; + } +} + +- (void) unregisterHotKeysWithTarget:(id)target { + NSPredicate * predicate = [NSPredicate predicateWithFormat:@"target = %@", target]; + [self unregisterHotKeysMatchingPredicate:predicate]; +} + +- (void) unregisterHotKeysWithTarget:(id)target action:(SEL)action { + NSPredicate * predicate = [NSPredicate predicateWithFormat:@"target = %@ AND actionString = %@", target, NSStringFromSelector(action)]; + [self unregisterHotKeysMatchingPredicate:predicate]; +} + +- (void) unregisterHotKeyWithKeyCode:(unsigned short)keyCode modifierFlags:(NSUInteger)flags { + NSPredicate * predicate = [NSPredicate predicateWithFormat:@"keyCode = %hu AND modifierFlags = %lu", keyCode, flags]; + [self unregisterHotKeysMatchingPredicate:predicate]; +} + +- (NSSet *) registeredHotKeys { + return [self hotKeysMatchingPredicate:[NSPredicate predicateWithFormat:@"hotKeyRef != NULL"]]; +} + +@end + +OSStatus dd_hotKeyHandler(EventHandlerCallRef nextHandler, EventRef theEvent, void * userData) { + NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; + + EventHotKeyID hotKeyID; + GetEventParameter(theEvent, kEventParamDirectObject, typeEventHotKeyID, NULL, sizeof(hotKeyID),NULL,&hotKeyID); + + UInt32 keyID = hotKeyID.id; + + NSSet * matchingHotKeys = [_registeredHotKeys filteredSetUsingPredicate:[NSPredicate predicateWithFormat:@"hotKeyID = %u", keyID]]; + if ([matchingHotKeys count] > 1) { NSLog(@"ERROR!"); } + _DDHotKey * matchingHotKey = [matchingHotKeys anyObject]; + + NSEvent * event = [NSEvent eventWithEventRef:theEvent]; + NSEvent * keyEvent = [NSEvent keyEventWithType:NSKeyUp + location:[event locationInWindow] + modifierFlags:[event modifierFlags] + timestamp:[event timestamp] + windowNumber:-1 + context:nil + characters:@"" + charactersIgnoringModifiers:@"" + isARepeat:NO + keyCode:[matchingHotKey keyCode]]; + + [matchingHotKey invokeWithEvent:keyEvent]; + + [pool release]; + + return noErr; +} + +UInt32 dd_translateModifierFlags(NSUInteger flags) { + UInt32 newFlags = 0; + if ((flags & NSControlKeyMask) > 0) { newFlags |= controlKey; } + if ((flags & NSCommandKeyMask) > 0) { newFlags |= cmdKey; } + if ((flags & NSShiftKeyMask) > 0) { newFlags |= shiftKey; } + if ((flags & NSAlternateKeyMask) > 0) { newFlags |= optionKey; } + return newFlags; +} + +NSString* dd_stringifyModifierFlags(NSUInteger flags) { + NSMutableArray * bits = [NSMutableArray array]; + if ((flags & NSControlKeyMask) > 0) { [bits addObject:@"NSControlKeyMask"]; } + if ((flags & NSCommandKeyMask) > 0) { [bits addObject:@"NSCommandKeyMask"]; } + if ((flags & NSShiftKeyMask) > 0) { [bits addObject:@"NSShiftKeyMask"]; } + if ((flags & NSAlternateKeyMask) > 0) { [bits addObject:@"NSAlternateKeyMask"]; } + if ([bits count] > 0) { + return [NSString stringWithFormat:@"(%@)", [bits componentsJoinedByString:@" | "]]; + } + return @"ERROR: No valid flags"; +} diff --git a/src/native/globalshortcut/javakey.h b/src/native/globalshortcut/javakey.h new file mode 100644 index 000000000..d3824e8b5 --- /dev/null +++ b/src/native/globalshortcut/javakey.h @@ -0,0 +1,163 @@ +/* + * Jitsi Communicator, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. + * See terms of license at gnu.org. + */ + +#ifndef JAVAKEY_H +#define JAVAKEY_H + +/** + * \enum java_keycode + * \brief Java keycode + */ +enum java_keycode +{ + JVK_PAUSE = 0x13, + JVK_CAPS_LOCK = 0x14, + JVK_ESCAPE = 0x1B, + JVK_SPACE =0x20, + JVK_UP = 0x26, + JVK_DOWN = 0x28, + JVK_LEFT = 0x25, + JVK_RIGHT = 0x27, + JVK_COMMA = 0x2C, + JVK_MINUS = 0x2D, + JVK_PERIOD = 0x2E, + JVK_SLASH = 0x2F, + JVK_0 = 0x30, + JVK_1 = 0x31, + JVK_2 = 0x32, + JVK_3 = 0x33, + JVK_4 = 0x34, + JVK_5 = 0x35, + JVK_6 = 0x36, + JVK_7 = 0x37, + JVK_8 = 0x38, + JVK_9 = 0x39, + JVK_SEMICOLON = 0x3B, + JVK_EQUALS = 0x3D, + JVK_A = 0x41, + JVK_B = 0x42, + JVK_C = 0x43, + JVK_D = 0x44, + JVK_E = 0x45, + JVK_F = 0x46, + JVK_G = 0x47, + JVK_H = 0x48, + JVK_I = 0x49, + JVK_J = 0x4A, + JVK_K = 0x4B, + JVK_L = 0x4C, + JVK_M = 0x4D, + JVK_N = 0x4E, + JVK_O = 0x4F, + JVK_P = 0x50, + JVK_Q = 0x51, + JVK_R = 0x52, + JVK_S = 0x53, + JVK_T = 0x54, + JVK_U = 0x55, + JVK_V = 0x56, + JVK_W = 0x57, + JVK_X = 0x58, + JVK_Y = 0x59, + JVK_Z = 0x5A, + JVK_OPEN_BRACKET = 0x5B, + JVK_BACK_SLASH = 0x5C, + JVK_CLOSE_BRACKET = 0x5D, + JVK_NUMPAD0 = 0x60, + JVK_NUMPAD1 = 0x61, + JVK_NUMPAD2 = 0x62, + JVK_NUMPAD3 = 0x63, + JVK_NUMPAD4 = 0x64, + JVK_NUMPAD5 = 0x65, + JVK_NUMPAD6 = 0x66, + JVK_NUMPAD7 = 0x67, + JVK_NUMPAD8 = 0x68, + JVK_NUMPAD9 = 0x69, + JVK_KP_UP = 0xE0, + JVK_KP_DOWN = 0xE1, + JVK_KP_LEFT = 0xE2, + JVK_KP_RIGHT = 0xE3, + JVK_MULTIPLY = 0x6A, + JVK_ADD = 0x6B, + JVK_SEPARATOR = 0x6C, + JVK_SUBTRACT = 0x6D, + JVK_DECIMAL = 0x6E, + JVK_DIVIDE = 0x6F, + JVK_DELETE = 0x7F, + JVK_NUM_LOCK = 0x90, + JVK_CLEAR = 0x03, + JVK_SCROLL_LOCK = 0x91, + JVK_F1 = 0x70, + JVK_F2 = 0x71, + JVK_F3 = 0x72, + JVK_F4 = 0x73, + JVK_F5 = 0x74, + JVK_F6= 0x75, + JVK_F7 = 0x76, + JVK_F8 = 0x77, + JVK_F9 = 0x78, + JVK_F10 = 0x79, + JVK_F11 = 0x7A, + JVK_F12 = 0x7B, + JVK_F13 = 0xF000, + JVK_F14 = 0xF001, + JVK_F15 = 0xF002, + JVK_F16 = 0xF003, + JVK_F17 = 0xF004, + JVK_F18 = 0xF005, + JVK_F19 = 0xF006, + JVK_F20 = 0xF007, + JVK_F21 = 0xF008, + JVK_F22 = 0xF009, + JVK_F23 = 0xF00A, + JVK_F24 = 0xF00B, + JVK_PRINTSCREEN = 0x9A, + JVK_INSERT = 0x9B, + JVK_HELP = 0x9C, + JVK_PAGE_UP = 0x21, + JVK_PAGE_DOWN = 0x22, + JVK_HOME = 0x24, + JVK_END = 0x23, + JVK_BACK_QUOTE = 0xC0, + JVK_QUOTE = 0xDE, + JVK_DEAD_GRAVE = 0x80, + JVK_DEAD_ACUTE = 0x81, + JVK_DEAD_CIRCUMFLEX = 0x82, + JVK_DEAD_TILDE = 0x83, + JVK_DEAD_MACRON = 0x84, + JVK_DEAD_BREVE = 0x85, + JVK_DEAD_ABOVEDOT = 0x86, + JVK_DEAD_DIAERESIS = 0x87, + JVK_DEAD_ABOVERING = 0x88, + JVK_DEAD_DOUBLEACUTE = 0x89, + JVK_DEAD_CARON = 0x8A, + JVK_DEAD_CEDILLA = 0x8B, + JVK_DEAD_OGONEK = 0x8C, + JVK_DEAD_IOTA = 0x8D, + JVK_AMPERSAND = 0x96, + JVK_ASTERISK = 0x97, + JVK_QUOTEDBL = 0x98, + JVK_LESS = 0x99, + JVK_GREATER = 0xA0, + JVK_BRACELEFT = 0xA1, + JVK_BRACERIGHT = 0xA2, + JVK_AT = 0x0200, + JVK_COLON = 0x0201, + JVK_CIRCUMFLEX = 0x0202, + JVK_DOLLAR = 0x0203, + JVK_EURO_SIGN = 0x0204, + JVK_EXCLAMATION_MARK = 0x0205, + JVK_INVERTED_EXCLAMATION_MARK = 0x0206, + JVK_LEFT_PARENTHESIS = 0x0207, + JVK_NUMBER_SIGN = 0x0208, + JVK_PLUS = 0x0209, + JVK_RIGHT_PARENTHESIS = 0x020A, + JVK_UNDERSCORE = 0x020B, +}; + +#endif /* JAVAKEY_H */ + diff --git a/src/native/globalshortcut/net_java_sip_communicator_impl_globalshortcut_NativeKeyboardHook.cc b/src/native/globalshortcut/net_java_sip_communicator_impl_globalshortcut_NativeKeyboardHook.cc new file mode 100644 index 000000000..350144807 --- /dev/null +++ b/src/native/globalshortcut/net_java_sip_communicator_impl_globalshortcut_NativeKeyboardHook.cc @@ -0,0 +1,924 @@ +/* + * Jitsi Communicator, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. + * See terms of license at gnu.org. + */ + +#include "net_java_sip_communicator_impl_globalshortcut_NativeKeyboardHook.h" + +/* Linux specific code */ + +#include +#include +#include + +#include + +#include +#include + +#include +#include +#include + +#include "javakey.h" + +/** + * \class keystrok + * \brief keystrok class. + */ +class keystrok +{ + public: + int vkcode; /**< Virtual keycode. */ + int modifiers; /**< Modifiers (ALT, CTLR, ...). */ + int active; /**< If the hotkey is active. */ +}; + +/** + * \class keyboard_hook + * \brief keyboard_hook structure. + */ +class keyboard_hook +{ + public: + Display* display; /**< X11 display. */ + Window root; /**< X11 root window. */ + jobject delegate; /**< Java object delegate. */ + JavaVM* jvm; /**< Java VM. */ + volatile int running; /**< Running state. */ + std::list keystrokes; /**< List of keystrokes registered. */ +}; + +/** + * \brief Convert Java keycode to native ones. + * \param keycode Java keycode + * \return native X11 keycode + */ +static int convertJavaKeycodeToX11(int keycode) +{ + int ret = -1; + switch(keycode) + { + /* 0 - 9 */ + case JVK_0: + ret = XK_0; + break; + case JVK_1: + ret = XK_1; + break; + case JVK_2: + ret = XK_2; + break; + case JVK_3: + ret = XK_3; + break; + case JVK_4: + ret = XK_4; + break; + case JVK_5: + ret = XK_5; + break; + case JVK_6: + ret = XK_6; + break; + case JVK_7: + ret = XK_7; + break; + case JVK_8: + ret = XK_8; + break; + case JVK_9: + ret = XK_9; + break; + /* A - Z */ + case JVK_A: + ret = XK_A; + break; + case JVK_B: + ret = XK_B; + break; + case JVK_C: + ret = XK_C; + break; + case JVK_D: + ret = XK_D; + break; + case JVK_E: + ret = XK_E; + break; + case JVK_F: + ret = XK_F; + break; + case JVK_G: + ret = XK_G; + break; + case JVK_H: + ret = XK_H; + break; + case JVK_I: + ret = XK_I; + break; + case JVK_J: + ret = XK_J; + break; + case JVK_K: + ret = XK_K; + break; + case JVK_L: + ret = XK_L; + break; + case JVK_M: + ret = XK_M; + break; + case JVK_N: + ret = XK_N; + break; + case JVK_O: + ret = XK_O; + break; + case JVK_P: + ret = XK_P; + break; + case JVK_Q: + ret = XK_Q; + break; + case JVK_R: + ret = XK_R; + break; + case JVK_S: + ret = XK_S; + break; + case JVK_T: + ret = XK_T; + break; + case JVK_U: + ret = XK_U; + break; + case JVK_V: + ret = XK_V; + break; + case JVK_W: + ret = XK_W; + break; + case JVK_X: + ret = XK_X; + break; + case JVK_Y: + ret = XK_Y; + break; + case JVK_Z: + ret = XK_Z; + break; + /* F1 - F12 */ + case JVK_F1: + ret = XK_F1; + break; + case JVK_F2: + ret = XK_F2; + break; + case JVK_F3: + ret = XK_F3; + break; + case JVK_F4: + ret = XK_F4; + break; + case JVK_F5: + ret = XK_F5; + break; + case JVK_F6: + ret = XK_F6; + break; + case JVK_F7: + ret = XK_F7; + break; + case JVK_F8: + ret = XK_F8; + break; + case JVK_F9: + ret = XK_F9; + break; + case JVK_F10: + ret = XK_F10; + break; + case JVK_F11: + ret = XK_F11; + break; + case JVK_F12: + ret = XK_F12; + break; + /* arrows (left, right, up, down) */ + case JVK_LEFT: + ret = XK_Left; + break; + case JVK_RIGHT: + ret = XK_Right; + break; + case JVK_UP: + ret = XK_Up; + break; + case JVK_DOWN: + ret = XK_Down; + break; + case JVK_COMMA: + ret = XK_comma; + break; + case JVK_MINUS: + ret = XK_minus; + break; + case JVK_PLUS: + ret = XK_plus; + break; + case JVK_PERIOD: + ret = XK_period; + break; + case JVK_SLASH: + ret = XK_slash; + break; + case JVK_BACK_SLASH: + ret = XK_backslash; + break; + case JVK_SEMICOLON: + ret = XK_semicolon; + break; + case JVK_EQUALS: + ret = XK_equal; + break; + case JVK_COLON: + ret = XK_colon; + break; + case JVK_UNDERSCORE: + ret = XK_underscore; + break; + case JVK_DOLLAR: + ret = XK_dollar; + break; + case JVK_EXCLAMATION_MARK: + ret = XK_exclam; + break; + case JVK_GREATER: + ret = XK_greater; + break; + case JVK_LESS: + ret = XK_less; + break; + case JVK_QUOTE: + ret = XK_quoteleft; + break; + case JVK_BACK_QUOTE: + ret = XK_quoteright; + break; + case JVK_INSERT: + ret = XK_Insert; + break; + case JVK_HELP: + ret = XK_Help; + break; + case JVK_HOME: + ret = XK_Home; + break; + case JVK_END: + ret = XK_End; + break; + case JVK_PAGE_UP: + ret = XK_Page_Up; + break; + case JVK_PAGE_DOWN: + ret = XK_Page_Down; + break; + case JVK_OPEN_BRACKET: + ret = XK_bracketleft; + break; + case JVK_CLOSE_BRACKET: + ret = XK_bracketright; + break; + case JVK_LEFT_PARENTHESIS: + ret = XK_parenleft; + break; + case JVK_RIGHT_PARENTHESIS: + ret = XK_parenright; + break; + case 0x08: + ret = XK_Delete; + break; + default: + break; + } + return ret; +} + +/** + * \brief Convert X11 keycode to Java ones. + * \param keycode X11 keycode + * \return Java keycode + */ +static int convertX11KeycodeToJava(int keycode) +{ + int ret = -1; + switch(keycode) + { + /* 0 - 9 */ + case XK_0: + ret = JVK_0; + break; + case XK_1: + ret = JVK_1; + break; + case XK_2: + ret = JVK_2; + break; + case XK_3: + ret = JVK_3; + break; + case XK_4: + ret = JVK_4; + break; + case XK_5: + ret = JVK_5; + break; + case XK_6: + ret = JVK_6; + break; + case XK_7: + ret = JVK_7; + break; + case XK_8: + ret = JVK_8; + break; + case XK_9: + ret = JVK_9; + break; + /* A - Z */ + case XK_A: + case XK_a: + ret = JVK_A; + break; + case XK_B: + case XK_b: + ret = JVK_B; + break; + case XK_C: + case XK_c: + ret = JVK_C; + break; + case XK_D: + case XK_d: + ret = JVK_D; + break; + case XK_E: + case XK_e: + ret = JVK_E; + break; + case XK_F: + case XK_f: + ret = JVK_F; + break; + case XK_G: + case XK_g: + ret = JVK_G; + break; + case XK_H: + case XK_h: + ret = JVK_H; + break; + case XK_I: + case XK_i: + ret = JVK_I; + break; + case XK_J: + case XK_j: + ret = JVK_J; + break; + case XK_K: + case XK_k: + ret = JVK_K; + break; + case XK_L: + case XK_l: + ret = JVK_L; + break; + case XK_M: + case XK_m: + ret = JVK_M; + break; + case XK_N: + case XK_n: + ret = JVK_N; + break; + case XK_O: + case XK_o: + ret = JVK_O; + break; + case XK_P: + case XK_p: + ret = JVK_P; + break; + case XK_Q: + case XK_q: + ret = JVK_Q; + break; + case XK_R: + case XK_r: + ret = JVK_R; + break; + case XK_S: + case XK_s: + ret = JVK_S; + break; + case XK_T: + case XK_t: + ret = JVK_T; + break; + case XK_U: + case XK_u: + ret = JVK_U; + break; + case XK_V: + case XK_v: + ret = JVK_V; + break; + case XK_W: + case XK_w: + ret = JVK_W; + break; + case XK_X: + case XK_x: + ret = JVK_X; + break; + case XK_Y: + case XK_y: + ret = JVK_Y; + break; + case XK_Z: + case XK_z: + ret = JVK_Z; + break; + /* F1 - F12 */ + case XK_F1: + ret = JVK_F1; + break; + case XK_F2: + ret = JVK_F2; + break; + case XK_F3: + ret = JVK_F3; + break; + case XK_F4: + ret = JVK_F4; + break; + case XK_F5: + ret = JVK_F5; + break; + case XK_F6: + ret = JVK_F6; + break; + case XK_F7: + ret = JVK_F7; + break; + case XK_F8: + ret = JVK_F8; + break; + case XK_F9: + ret = JVK_F9; + break; + case XK_F10: + ret = JVK_F10; + break; + case XK_F11: + ret = JVK_F11; + break; + case XK_F12: + ret = JVK_F12; + break; + /* arrows (left, right, up, down) */ + case XK_Left: + ret = JVK_LEFT; + break; + case XK_Right: + ret = JVK_RIGHT; + break; + case XK_Up: + ret = JVK_UP; + break; + case XK_Down: + ret = JVK_DOWN; + break; + case XK_comma: + ret = JVK_COMMA; + break; + case XK_minus: + ret = JVK_MINUS; + break; + case XK_plus: + ret = JVK_PLUS; + break; + case XK_period: + ret = JVK_PERIOD; + break; + case XK_slash: + ret = JVK_SLASH; + break; + case XK_backslash: + ret = JVK_BACK_SLASH; + break; + case XK_semicolon: + ret = JVK_SEMICOLON; + break; + case XK_equal: + ret = JVK_EQUALS; + break; + case XK_colon: + ret = JVK_COLON; + break; + case XK_underscore: + ret = JVK_UNDERSCORE; + break; + case XK_dollar: + ret = JVK_DOLLAR; + break; + case XK_exclam: + ret = JVK_EXCLAMATION_MARK; + break; + case XK_greater: + ret = JVK_GREATER; + break; + case XK_less: + ret = JVK_LESS; + break; + case XK_quoteleft: + ret = JVK_QUOTE; + break; + case XK_quoteright: + ret = JVK_BACK_QUOTE; + break; + case XK_Insert: + ret = JVK_INSERT; + break; + case XK_Help: + ret = JVK_HELP; + break; + case XK_Home: + ret = JVK_HOME; + break; + case XK_End: + ret = JVK_END; + break; + case XK_Page_Up: + ret = JVK_PAGE_UP; + break; + case XK_Page_Down: + ret = JVK_PAGE_DOWN; + break; + case JVK_OPEN_BRACKET: + ret = XK_bracketleft; + break; + case XK_bracketright: + ret = JVK_CLOSE_BRACKET; + break; + case XK_parenleft: + ret = JVK_LEFT_PARENTHESIS; + break; + case XK_parenright: + ret = JVK_RIGHT_PARENTHESIS; + break; + case XK_Delete: + ret = 0x08; + break; + default: + break; + } + return ret; +} + + +/** + * \brief Convert X11 modifiers to Java user-defined ones. + * \param modfiers X11 modifiers + * \return Java user-defined modifiers + */ +static int X11ModifiersToJavaUserDefined(int modifiers) +{ + int javaModifiers = 0; + + if(modifiers & ControlMask) + { + javaModifiers |= 0x01; + } + if(modifiers & Mod1Mask) + { + /* Alt */ + javaModifiers |= 0x02; + } + if(modifiers & ShiftMask) + { + javaModifiers |= 0x04; + } + if(modifiers & Mod4Mask) + { + /* Super */ + javaModifiers |= 0x08; + } + + return javaModifiers; +} + +/** + * \brief Convert Java user-defined modifiers to X11 ones. + * \param modifiers Java user-defined modifiers + * \return X11 modifiers + */ +static int JavaUserDefinedModifiersToX11(int modifiers) +{ + int x11Modifiers = 0; + + if(modifiers & 0x01) + { + x11Modifiers |= ControlMask; + } + if(modifiers & 0x02) + { + /* Alt */ + x11Modifiers |= Mod1Mask; + } + if(modifiers & 0x04) + { + x11Modifiers |= ShiftMask; + } + if(modifiers & 0x08) + { + /* Super */ + x11Modifiers |= Mod4Mask; + } + + return x11Modifiers; +} + +/** + * \brief Notify Java side about key pressed (keycode + modifiers). + * \param keycode keycode + * \param modifiers modifiers used (SHIFT, CTRL, ALT, LOGO) + */ +static void notify(struct keyboard_hook* keyboard, jint keycode, jint modifiers) +{ + JNIEnv *jniEnv = NULL; + jclass delegateClass = NULL; + + if(!keyboard->delegate) + { + return; + } + + if(0 != keyboard->jvm->AttachCurrentThreadAsDaemon((void **)&jniEnv, NULL)) + { + return; + } + + delegateClass = jniEnv->GetObjectClass(keyboard->delegate); + + if(delegateClass) + { + jmethodID methodid = NULL; + + methodid = jniEnv->GetMethodID(delegateClass, "receiveKey", "(II)V"); + + if(methodid) + { + jniEnv->CallVoidMethod(keyboard->delegate, methodid, keycode, modifiers); + } + } + jniEnv->ExceptionClear(); +} + +/** + * \brief X11 event loop thread entry point. + * \param arg thread argument + * \return NULL + */ +static void* x11_event_loop_thread(void* arg) +{ + struct keyboard_hook* keyboard = (struct keyboard_hook*)arg; + + XSelectInput(keyboard->display, keyboard->root, KeyPressMask); + + while(keyboard->running) + { + XEvent ev; + while(XCheckMaskEvent(keyboard->display, 0xFFFFFFFF, &ev)) + { + switch (ev.type) + { + case KeyPress: + for(std::list::iterator it = keyboard->keystrokes.begin() ; it != keyboard->keystrokes.end() ; ++it) + { + keystrok& ks = (*it); + XKeyEvent* keyEvent = (XKeyEvent*)&ev.xkey; + unsigned long keycode = -1; + //XKeycodeToKeysym(keyboard->display, keyEvent->keycode, 1); + + XLookupString(keyEvent, NULL, 0, &keycode, NULL); + + keycode = convertX11KeycodeToJava(keycode); + int modifiers = X11ModifiersToJavaUserDefined(keyEvent->state); + + if(ks.vkcode == keycode && ks.modifiers == modifiers) + { + notify(keyboard, ks.vkcode, ks.modifiers); + } + } + + break; + default: + break; + } + } + + for(std::list::iterator it = keyboard->keystrokes.begin() ; it != keyboard->keystrokes.end() ; ++it) + { + keystrok& ks = (*it); + + if(ks.active == 0) + { + /* hotkey to add */ + int x11Keycode = convertJavaKeycodeToX11(ks.vkcode); + if(x11Keycode != -1) + { + x11Keycode = XKeysymToKeycode(keyboard->display, x11Keycode); + } + else + { + printf("failed\n");fflush(stdout); + ks.active = -1; + continue; + } + int x11Modifiers = JavaUserDefinedModifiersToX11(ks.modifiers); + + ks.active = 1; + if(XGrabKey(keyboard->display, x11Keycode, x11Modifiers, keyboard->root, False, GrabModeAsync, GrabModeAsync) > 1) + { + fprintf(stderr, "[LOOP] Error when XGrabKey\n");fflush(stderr); + ks.active = -1; + } + } + else if(ks.active == -1) + { + /* hotkey to remove */ + int x11Keycode = XKeysymToKeycode(keyboard->display, + convertJavaKeycodeToX11(ks.vkcode)); + int x11Modifiers = JavaUserDefinedModifiersToX11(ks.modifiers); + + if(XUngrabKey(keyboard->display, x11Keycode, x11Modifiers, keyboard->root) > 1) + { + fprintf(stderr, "[LOOP] Error when XUngrabKey\n");fflush(stderr); + } + it = keyboard->keystrokes.erase(it)--; + } + } + + usleep(1000 * 1000); + pthread_yield(); + } + + return NULL; +} + +JNIEXPORT jlong JNICALL Java_net_java_sip_communicator_impl_globalshortcut_NativeKeyboardHook_init + (JNIEnv* jniEnv, jclass clazz) +{ + struct keyboard_hook* keyboard = NULL; + + (void)jniEnv; + (void)clazz; + + keyboard = new keyboard_hook(); + if(!keyboard) + { + return (jlong)NULL; + } + + keyboard->display = XOpenDisplay(NULL); + + if(!keyboard->display) + { + free(keyboard); + return (jlong)NULL; + } + + keyboard->root = DefaultRootWindow(keyboard->display); + + return (jlong)keyboard; +} + +/* XXX release JNI method */ + +JNIEXPORT void JNICALL Java_net_java_sip_communicator_impl_globalshortcut_NativeKeyboardHook_start + (JNIEnv* jniEnv, jclass clazz, jlong ptr) +{ + struct keyboard_hook* keyboard = (struct keyboard_hook*)ptr; + pthread_t id; + pthread_attr_t attr; + + (void)jniEnv; + (void)clazz; + + if(keyboard->running) + { + return; + } + + pthread_attr_init(&attr); + + if(pthread_attr_setdetachstate(&attr, 1) != 0) + { + perror("pthread_attr_setdetachstate");fflush(stderr); + return; + } + + keyboard->running = 1; + + if(pthread_create(&id, &attr, x11_event_loop_thread, keyboard) != 0) + { + perror("pthread_create");fflush(stderr); + return; + } +} + +JNIEXPORT void JNICALL Java_net_java_sip_communicator_impl_globalshortcut_NativeKeyboardHook_stop + (JNIEnv* jniEnv, jclass clazz, jlong ptr) +{ + struct keyboard_hook* keyboard = (struct keyboard_hook*)ptr; + + (void)jniEnv; + (void)clazz; + + keyboard->running = 0; +} + +JNIEXPORT void JNICALL Java_net_java_sip_communicator_impl_globalshortcut_NativeKeyboardHook_setDelegate + (JNIEnv* jniEnv, jclass clazz, jlong ptr, jobject delegate) +{ + struct keyboard_hook* keyboard = (struct keyboard_hook*)ptr; + + (void)clazz; + + if(keyboard->delegate) + { + jniEnv->DeleteGlobalRef(keyboard->delegate); + keyboard->delegate = NULL; + } + + if(delegate) + { + jobject delegate2 = jniEnv->NewGlobalRef(delegate); + if(delegate2) + { + jniEnv->GetJavaVM(&keyboard->jvm); + keyboard->delegate = delegate2; + } + } +} + +JNIEXPORT jboolean JNICALL Java_net_java_sip_communicator_impl_globalshortcut_NativeKeyboardHook_registerShortcut + (JNIEnv* jniEnv, jclass clazz, jlong ptr, jint keycode, jint modifiers) +{ + struct keyboard_hook* keyboard = (struct keyboard_hook*)ptr; + + (void)jniEnv; + (void)clazz; + + if(keyboard) + { + keystrok ks; + + ks.vkcode = keycode; + ks.modifiers = modifiers; + ks.active = 0; + keyboard->keystrokes.push_back(ks); + + return JNI_TRUE; + } + + return JNI_FALSE; +} + +JNIEXPORT void JNICALL Java_net_java_sip_communicator_impl_globalshortcut_NativeKeyboardHook_unregisterShortcut + (JNIEnv* jniEnv, jclass clazz, jlong ptr, jint keycode, jint modifiers) +{ + struct keyboard_hook* keyboard = (struct keyboard_hook*)ptr; + + (void)jniEnv; + (void)clazz; + + if(keyboard) + { + for(std::list::iterator it = keyboard->keystrokes.begin() ; it != keyboard->keystrokes.end() ; ++it) + { + keystrok& ks = (*it); + if(ks.vkcode == keycode && ks.modifiers == modifiers) + { + ks.active = -1; + } + } + } +} + diff --git a/src/native/globalshortcut/net_java_sip_communicator_impl_globalshortcut_NativeKeyboardHook.cpp b/src/native/globalshortcut/net_java_sip_communicator_impl_globalshortcut_NativeKeyboardHook.cpp new file mode 100644 index 000000000..119b51cc6 --- /dev/null +++ b/src/native/globalshortcut/net_java_sip_communicator_impl_globalshortcut_NativeKeyboardHook.cpp @@ -0,0 +1,1019 @@ +/* + * Jitsi Communicator, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. + * See terms of license at gnu.org. + */ + +#include "net_java_sip_communicator_impl_globalshortcut_NativeKeyboardHook.h" + +#include +#include +#include + +#include "javakey.h" + +/* MS Windows specific code */ + +/** + * \def WIN32_LEAN_AND_MEAN + * \brief Exclude not commonly used headers from win32 API. + * + * It excludes some unused stuff from windows headers and + * by the way code compiles faster. + */ +#define WIN32_LEAN_AND_MEAN + +#include +#include + +/** + * \def WINDOW_SHORTCUT_NAME + * \brief Name of the window. + */ +#define WINDOW_SHORTCUT_NAME L"Jitsi Global Shortcut hook" + +/** + * \class keystrok + * \brief keystrok class. + */ +class keystrok +{ + public: + DWORD vkcode; /**< Virtual keycode. */ + int modifiers; /**< Modifiers (ALT, CTLR, ...). */ + int id; /**< ID. */ + int active; /**< If the hotkey is active. */ +}; + +/** + * \class keyboard_hook + * \brief keyboard_hook class. + */ +class keyboard_hook +{ + public: + int thread_id; /**< Thread ID. */ + jobject delegate; /**< Java object delegate. */ + JavaVM* jvm; /**< Java VM. */ + int running; /**< Running state. */ + HHOOK hook; /**< Windows keyboard hook. */ + HWND hwnd; /**< Handle of the window that will receive event. */ + int hotkey_next_id; /**< Next ID to use for a new hotkey. */ + std::list keystrokes; /**< List of keystrokes registered. */ +}; + +/** + * \enum windows_keycode + * \brief Windows keycode. + */ +enum jwindows_keycode +{ + //VK_ADD = 0xC1, + //VK_BACK = 0x08, + //VK_CLEAR = 0x0C, + //VK_DIVIDE = 0x6F, + //VK_ICO_HELP = 0xE3, + VK_KEY_0 = 0x30, + VK_KEY_1 = 0x31, + VK_KEY_2 = 0x32, + VK_KEY_3 = 0x33, + VK_KEY_4 = 0x34, + VK_KEY_5 = 0x35, + VK_KEY_6 = 0x36, + VK_KEY_7 = 0x37, + VK_KEY_8 = 0x38, + VK_KEY_9 = 0x39, + VK_KEY_A = 0x41, + VK_KEY_B = 0x42, + VK_KEY_C = 0x43, + VK_KEY_D = 0x44, + VK_KEY_E = 0x45, + VK_KEY_F = 0x46, + VK_KEY_G = 0x47, + VK_KEY_H = 0x48, + VK_KEY_I = 0x49, + VK_KEY_J = 0x4A, + VK_KEY_K = 0x4B, + VK_KEY_L = 0x4C, + VK_KEY_M = 0x4D, + VK_KEY_N = 0x4E, + VK_KEY_O = 0x4F, + VK_KEY_P = 0x50, + VK_KEY_Q = 0x51, + VK_KEY_R = 0x52, + VK_KEY_S = 0x53, + VK_KEY_T = 0x54, + VK_KEY_U = 0x55, + VK_KEY_V = 0x56, + VK_KEY_W = 0x57, + VK_KEY_X = 0x58, + VK_KEY_Y = 0x59, + VK_KEY_Z = 0x5A, + //VK_MULTIPLY = 0x6A + //VK_OEM_1 = 0xBA, + //VK_OEM_2 = 0xBF, + //VK_OEM_3 = 0xC0, + //VK_OEM_4 = 0xDB, + //VK_OEM_5 = 0xDC, + //VK_OEM_6 = 0xDD, + //VK_OEM_7 = 0xDE, + //VK_OEM_8 = 0xDF, + //VK_OEM_PERIOD = 0xBE, + //VK_OEM_PLUS = 0xBB, + //VK_F1 = 0x70, + //VK_F10 = 0x79, + //VK_F11 = 0x7A, + //VK_F12 = 0x7B, + //VK_F2 = 0x71, + //VK_F3 = 0x72, + //VK_F4 = 0x73, + //VK_F5 = 0x74, + //VK_F6 = 0x75, + //VK_F7 = 0x76, + //VK_F8 = 0x77, + //VK_F9 = 0x78, + //VK_DELETE = 0x2E, + //VK_HELP = 0x2F, + //VK_HOME = 0x24, + //VK_PRIOR = 0x21 + //VK_NEXT = 0x22 + //VK_END = 0x23, + //VK_DOWN = 0x28, + //VK_RIGHT = 0x27, + //VK_UP = 0x26, + //VK_LEFT = 0x25, +}; + +static const int VK_COLON = VkKeyScan(':'); +static const int VK_SEMICOLON = VkKeyScan(';'); +static const int VK_PERIOD = VkKeyScan('.'); +static const int VK_DOLLAR = VkKeyScan('$'); +static const int VK_GREATER = VkKeyScan('>'); +static const int VK_LESS = VkKeyScan('<'); +static const int VK_SLASH = VkKeyScan('/'); + +/** + * \var g_keyboard_hook + * \brief Pointer to keyboard_hook. + */ +static keyboard_hook g_keyboard_hook; + +/** + * \brief Convert Java keycode to native ones. + * \param keycode Java keycode + * \return native Windows keycode + */ +static int convertJavaKeycodeToWindows(int keycode) +{ + int ret = -1; + switch(keycode) + { + /* 0 - 9 */ + case JVK_0: + ret = VK_KEY_0; + break; + case JVK_1: + ret = VK_KEY_1; + break; + case JVK_2: + ret = VK_KEY_2; + break; + case JVK_3: + ret = VK_KEY_3; + break; + case JVK_4: + ret = VK_KEY_4; + break; + case JVK_5: + ret = VK_KEY_5; + break; + case JVK_6: + ret = VK_KEY_6; + break; + case JVK_7: + ret = VK_KEY_7; + break; + case JVK_8: + ret = VK_KEY_8; + break; + case JVK_9: + ret = VK_KEY_9; + break; + /* A - Z */ + case JVK_A: + ret = VK_KEY_A; + break; + case JVK_B: + ret = VK_KEY_B; + break; + case JVK_C: + ret = VK_KEY_C; + break; + case JVK_D: + ret = VK_KEY_D; + break; + case JVK_E: + ret = VK_KEY_E; + break; + case JVK_F: + ret = VK_KEY_F; + break; + case JVK_G: + ret = VK_KEY_G; + break; + case JVK_H: + ret = VK_KEY_H; + break; + case JVK_I: + ret = VK_KEY_I; + break; + case JVK_J: + ret = VK_KEY_J; + break; + case JVK_K: + ret = VK_KEY_K; + break; + case JVK_L: + ret = VK_KEY_L; + break; + case JVK_M: + ret = VK_KEY_M; + break; + case JVK_N: + ret = VK_KEY_N; + break; + case JVK_O: + ret = VK_KEY_O; + break; + case JVK_P: + ret = VK_KEY_P; + break; + case JVK_Q: + ret = VK_KEY_Q; + break; + case JVK_R: + ret = VK_KEY_R; + break; + case JVK_S: + ret = VK_KEY_S; + break; + case JVK_T: + ret = VK_KEY_T; + break; + case JVK_U: + ret = VK_KEY_U; + break; + case JVK_V: + ret = VK_KEY_V; + break; + case JVK_W: + ret = VK_KEY_W; + break; + case JVK_X: + ret = VK_KEY_X; + break; + case JVK_Y: + ret = VK_KEY_Y; + break; + case JVK_Z: + ret = VK_KEY_Z; + break; + /* F1 - F12 */ + case JVK_F1: + ret = VK_F1; + break; + case JVK_F2: + ret = VK_F2; + break; + case JVK_F3: + ret = VK_F3; + break; + case JVK_F4: + ret = VK_F4; + break; + case JVK_F5: + ret = VK_F5; + break; + case JVK_F6: + ret = VK_F6; + break; + case JVK_F7: + ret = VK_F7; + break; + case JVK_F8: + ret = VK_F8; + break; + case JVK_F9: + ret = VK_F9; + break; + case JVK_F10: + ret = VK_F10; + break; + case JVK_F11: + ret = VK_F11; + break; + case JVK_F12: + ret = VK_F12; + break; + /* arrows (left, right, up, down) */ + case JVK_LEFT: + ret = VK_LEFT; + break; + case JVK_RIGHT: + ret = VK_RIGHT; + break; + case JVK_UP: + ret = VK_UP; + break; + case JVK_DOWN: + ret = VK_DOWN; + break; + case JVK_COMMA: + ret = VK_OEM_COMMA; + break; + case JVK_MINUS: + ret = VK_OEM_MINUS; + break; + case JVK_PLUS: + ret = VK_ADD; + break; + case JVK_PERIOD: + ret = VK_PERIOD; + break; + case JVK_SLASH: + ret = VK_SLASH; + break; + case JVK_BACK_SLASH: + ret = VK_OEM_5; + break; + case JVK_SEMICOLON: + ret = VK_SEMICOLON; + break; + case JVK_EQUALS: + ret = VK_OEM_PLUS; + break; + case JVK_COLON: + ret = VK_COLON; + break; + case JVK_UNDERSCORE: + ret = -1; + break; + case JVK_DOLLAR: + ret = VK_DOLLAR; + break; + case JVK_EXCLAMATION_MARK: + ret = VK_OEM_8; + break; + case JVK_LESS: + ret = VK_LESS; + break; + case JVK_GREATER: + ret = VK_GREATER; + break; + case JVK_QUOTE: + ret = -1; + break; + case JVK_BACK_QUOTE: + ret = -1; + break; + case JVK_INSERT: + ret = VK_INSERT; + break; + case JVK_HELP: + ret = VK_ICO_HELP; + break; + case JVK_HOME: + ret = VK_HOME; + break; + case JVK_END: + ret = VK_END; + break; + case JVK_PAGE_UP: + ret = VK_PRIOR; + break; + case JVK_PAGE_DOWN: + ret = VK_NEXT; + break; + case JVK_OPEN_BRACKET: + ret = VK_OEM_4; + break; + case JVK_CLOSE_BRACKET: + ret = VK_OEM_6; + break; + case 0x08: + ret = VK_DELETE; + break; + default: + break; + } + + return ret; +} + +/** + * \brief Convert Windows keycode to Java ones. + * \param keycode Windows keycode + * \return Java keycode + */ +static int convertWindowsKeycodeToJava(int keycode) +{ + int ret = -1; + switch(keycode) + { + /* 0 - 9 */ + case VK_KEY_0: + ret = JVK_0; + break; + case VK_KEY_1: + ret = JVK_1; + break; + case VK_KEY_2: + ret = JVK_2; + break; + case VK_KEY_3: + ret = JVK_3; + break; + case VK_KEY_4: + ret = JVK_4; + break; + case VK_KEY_5: + ret = JVK_5; + break; + case VK_KEY_6: + ret = JVK_6; + break; + case VK_KEY_7: + ret = JVK_7; + break; + case VK_KEY_8: + ret = JVK_8; + break; + case VK_KEY_9: + ret = JVK_9; + break; + /* A - Z */ + case VK_KEY_A: + ret = JVK_A; + break; + case VK_KEY_B: + ret = JVK_B; + break; + case VK_KEY_C: + ret = JVK_C; + break; + case VK_KEY_D: + ret = JVK_D; + break; + case VK_KEY_E: + ret = JVK_E; + break; + case VK_KEY_F: + ret = JVK_F; + break; + case VK_KEY_G: + ret = JVK_G; + break; + case VK_KEY_H: + ret = JVK_H; + break; + case VK_KEY_I: + ret = JVK_I; + break; + case VK_KEY_J: + ret = JVK_J; + break; + case VK_KEY_K: + ret = JVK_K; + break; + case VK_KEY_L: + ret = JVK_L; + break; + case VK_KEY_M: + ret = JVK_M; + break; + case VK_KEY_N: + ret = JVK_N; + break; + case VK_KEY_O: + ret = JVK_O; + break; + case VK_KEY_P: + ret = JVK_P; + break; + case VK_KEY_Q: + ret = JVK_Q; + break; + case VK_KEY_R: + ret = JVK_R; + break; + case VK_KEY_S: + ret = JVK_S; + break; + case VK_KEY_T: + ret = JVK_T; + break; + case VK_KEY_U: + ret = JVK_U; + break; + case VK_KEY_V: + ret = JVK_V; + break; + case VK_KEY_W: + ret = JVK_W; + break; + case VK_KEY_X: + ret = JVK_X; + break; + case VK_KEY_Y: + ret = JVK_Y; + break; + case VK_KEY_Z: + ret = JVK_Z; + break; + /* F1 - F12 */ + case VK_F1: + ret = JVK_F1; + break; + case VK_F2: + ret = JVK_F2; + break; + case VK_F3: + ret = JVK_F3; + break; + case VK_F4: + ret = JVK_F4; + break; + case VK_F5: + ret = JVK_F5; + break; + case VK_F6: + ret = JVK_F6; + break; + case VK_F7: + ret = JVK_F7; + break; + case VK_F8: + ret = JVK_F8; + break; + case VK_F9: + ret = JVK_F9; + break; + case VK_F10: + ret = JVK_F10; + break; + case VK_F11: + ret = JVK_F11; + break; + case VK_F12: + ret = JVK_F12; + break; + /* arrows (left, right, up, down) */ + case VK_LEFT: + ret = JVK_LEFT; + break; + case VK_RIGHT: + ret = JVK_RIGHT; + break; + case VK_UP: + ret = JVK_UP; + break; + case VK_DOWN: + ret = JVK_DOWN; + break; + case VK_OEM_COMMA: + ret = JVK_COMMA; + break; + case VK_OEM_MINUS: + ret = JVK_MINUS; + break; + case VK_ADD: + ret = JVK_PLUS; + break; + //case VK_OEM_PERIOD: + // ret = JVK_PERIOD; + // break; + case VK_DIVIDE: + ret = JVK_SLASH; + break; + case VK_OEM_5: + ret = JVK_BACK_SLASH; + break; + //case VK_OEM_1: + // ret = JVK_SEMICOLON; + // break; + case VK_OEM_PLUS: + ret = JVK_EQUALS; + break; + case VK_OEM_8: + ret = JVK_EXCLAMATION_MARK; + break; + //case VK_OEM_PERIOD: + // ret = JVK_GREATER; + // break; + //case VK_OEM_COMMA: + // ret = JVK_LESS; + // break; + case VK_INSERT: + ret = JVK_INSERT; + break; + case VK_HELP: + ret = JVK_HELP; + break; + case VK_HOME: + ret = JVK_HOME; + break; + case VK_END: + ret = JVK_END; + break; + case VK_PRIOR: + ret = JVK_PAGE_UP; + break; + case VK_NEXT: + ret = JVK_PAGE_DOWN; + break; + case VK_OEM_4: + ret = JVK_OPEN_BRACKET; + break; + case VK_OEM_6: + ret = JVK_CLOSE_BRACKET; + break; + case VK_DELETE: + ret = 0x08; + break; + default: + break; + } + if(ret == -1) + { + if(keycode == VK_COLON) + ret = JVK_COLON; + else if(keycode == VK_SEMICOLON) + ret = JVK_SEMICOLON; + else if(keycode == VK_PERIOD) + ret = JVK_PERIOD; + else if(keycode == VK_DOLLAR) + ret = JVK_DOLLAR; + else if(keycode == VK_LESS) + ret = JVK_LESS; + else if(keycode == VK_GREATER) + ret = JVK_GREATER; + else if(keycode == VK_SLASH) + ret = JVK_SLASH; + } + return ret; +} + +/** + * \brief Convert Windows modifiers to our Java user-defined ones. + * \param modifiers Windows modifiers (MOD_CONTROL, ...) + * \return Java user-defined modifiers + */ +static int convertWindowsModifiersToJavaUserDefined(int modifiers) +{ + int javaModifiers = 0; + + if(modifiers & MOD_CONTROL) + { + /* CTRL */ + javaModifiers |= 0x01; + } + if(modifiers & MOD_ALT) + { + /* ALT */ + javaModifiers |= 0x02; + } + if(modifiers & MOD_SHIFT) + { + /* SHIFT */ + javaModifiers |= 0x04; + } + if(modifiers & MOD_WIN) + { + /* LOGO */ + javaModifiers |= 0x08; + } + + return javaModifiers; +} + +/** + * \brief Convert Java user-defined modifiers to Windows ones. + * \param modifiers Java user-defined modifiers + * \return Windows modifiers + */ +static int convertJavaUserDefinedModifiersToWindows(int modifiers) +{ + int winModifiers = 0; + + if(modifiers & 0x01) + { + /* CTRL */ + winModifiers |= MOD_CONTROL; + } + if(modifiers & 0x02) + { + /* ALT */ + winModifiers |= MOD_ALT; + } + if(modifiers & 0x04) + { + /* SHIFT */ + winModifiers |= MOD_SHIFT; + } + if(modifiers & 0x08) + { + /* LOGO */ + winModifiers |= MOD_WIN; + } + + return winModifiers; +} + +/** + * \brief Notify Java side about key pressed (keycode + modifiers). + * \param keycode keycode + * \param modifiers modifiers used (SHIFT, CTRL, ALT, LOGO) + */ +static void notify(jint keycode, jint modifiers) +{ + JNIEnv *jniEnv = NULL; + jclass delegateClass = NULL; + + if(!g_keyboard_hook.delegate) + { + return; + } + + if(0 != g_keyboard_hook.jvm->AttachCurrentThreadAsDaemon((void **)&jniEnv, NULL)) + { + return; + } + + delegateClass = jniEnv->GetObjectClass(g_keyboard_hook.delegate); + + if(delegateClass) + { + jmethodID methodid = NULL; + + methodid = jniEnv->GetMethodID(delegateClass, "receiveKey", "(II)V"); + + if(methodid) + { + jniEnv->CallVoidMethod(g_keyboard_hook.delegate, methodid, keycode, modifiers); + } + } + jniEnv->ExceptionClear(); +} + +/** + * \brief Called in WndProc to check the event received. + * \param wParam wparam + * \param lParam lparam + * \return 0 if hotkey is processed, -1 otherwise + */ +HRESULT callback(UINT msg, WPARAM wParam, LPARAM lParam) +{ + keyboard_hook* keyboard = &g_keyboard_hook; + + for(std::list::iterator it = keyboard->keystrokes.begin() ; it != keyboard->keystrokes.end() ; ++it) + { + keystrok& ks = (*it); + + if(ks.active == 0) + { + /* hotkey to add */ + ks.active = 1; + + if(!RegisterHotKey(keyboard->hwnd, ks.id, ks.modifiers, ks.vkcode)) + { + fprintf(stderr, "[LOOP] Problem with RegisterHotKey: %d\n", GetLastError());fflush(stderr); + ks.active = -1; + } + } + else if(ks.active == -1) + { + /* hotkey to remove */ + if(!UnregisterHotKey(keyboard->hwnd, ks.id)) + { + //fprintf(stderr, "[LOOP] Error when UnregisterHotKey: %d\n", GetLastError());fflush(stderr); + } + it = keyboard->keystrokes.erase(it)--; + } + } + + if(msg == WM_HOTKEY) + { + for(std::list::iterator it = g_keyboard_hook.keystrokes.begin() ; it != g_keyboard_hook.keystrokes.end() ; ++it) + { + keystrok ks = (*it); + + /* check via hotkey id */ + if(ks.id == wParam) + { + int javaModifiers = 0; + int javaKeycode = 0; + + javaKeycode = convertWindowsKeycodeToJava(HIWORD(lParam)); //ks.vkcode); + javaModifiers = convertWindowsModifiersToJavaUserDefined(ks.modifiers); + notify(javaKeycode, javaModifiers); + return 0; + } + } + } + return -1; +} + +/** + * \brief Windows UI procedure. + * \param hWnd handle of the window + * \param wParam wparam + * \param lParam lparam + * \return 0 + */ +LRESULT CALLBACK WndProcW(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) +{ + long res = callback(msg, wParam, lParam); + + if(res != -1) + { + return res; + } + + return DefWindowProcW(hWnd, msg, wParam, lParam); +} + +/** + * \brief Register the Win32 Window class. + * \param hInstance instance of the program + */ +static void RegisterWindowClassW(HINSTANCE hInstance) +{ + WNDCLASSEXW wcex; + + memset(&wcex, 0x00, sizeof(WNDCLASSEXW)); + wcex.cbSize = sizeof(WNDCLASSEXW); + wcex.style = CS_HREDRAW | CS_VREDRAW; + wcex.lpfnWndProc = WndProcW; + wcex.cbClsExtra = 0; + wcex.cbWndExtra = 0; + wcex.hInstance = hInstance; + wcex.hIcon = 0; + wcex.hCursor = 0; + wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); + wcex.lpszMenuName = 0; + wcex.lpszClassName = WINDOW_SHORTCUT_NAME; + wcex.hIconSm = 0; + + if(RegisterClassExW(&wcex) == 0) + { + fprintf(stderr, "Failed to register window class: %d", GetLastError()); + fflush(stderr); + } +} + +/** + * \brief Thread that create window and that monitor event related to it. + * \param pThreadParam thread parameter + * \return 0 + */ +static unsigned WINAPI CreateWndThreadW(LPVOID pThreadParam) +{ + HINSTANCE hInstance = GetModuleHandle(NULL); + keyboard_hook* keyboard = (keyboard_hook*)pThreadParam; + + RegisterWindowClassW(hInstance); + + HWND hWnd = CreateWindowW(WINDOW_SHORTCUT_NAME, NULL, WS_OVERLAPPEDWINDOW, + CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, + NULL, NULL, hInstance, NULL); + + if(hWnd == NULL) + { + fprintf(stderr, "Failed to create window: %d\n", GetLastError()); + fflush(stderr); + return 0; + } + else + { + MSG msg; + + keyboard->hwnd = hWnd; + + while(GetMessageW(&msg, hWnd, 0, 0)) + { + TranslateMessage(&msg); + DispatchMessageW(&msg); + } + return msg.wParam; + } +} + +JNIEXPORT jlong JNICALL Java_net_java_sip_communicator_impl_globalshortcut_NativeKeyboardHook_init + (JNIEnv* jniEnv, jclass clazz) +{ + g_keyboard_hook.jvm = 0; + g_keyboard_hook.delegate = 0; + g_keyboard_hook.running = 0; + g_keyboard_hook.hotkey_next_id = 0xC000; + return (jlong)&g_keyboard_hook; +} + +JNIEXPORT void JNICALL Java_net_java_sip_communicator_impl_globalshortcut_NativeKeyboardHook_start + (JNIEnv* jniEnv, jclass clazz, jlong ptr) +{ + keyboard_hook* keyboard = (keyboard_hook*)ptr; + HANDLE hThread = NULL; + UINT uThreadId = 0; + + hThread = (HANDLE)_beginthreadex(NULL, 0, &CreateWndThreadW, keyboard, 0, &uThreadId); + if(!hThread) + { + fprintf(stderr, "Problem creating globalshortcut thread\n");fflush(stderr); + keyboard->running = 0; + return; + } +} + +JNIEXPORT void JNICALL Java_net_java_sip_communicator_impl_globalshortcut_NativeKeyboardHook_stop + (JNIEnv* jniEnv, jclass clazz, jlong ptr) +{ + keyboard_hook* keyboard = (keyboard_hook*)ptr; + + keyboard->hook = 0; + keyboard->running = 0; +} + +JNIEXPORT void JNICALL Java_net_java_sip_communicator_impl_globalshortcut_NativeKeyboardHook_setDelegate + (JNIEnv* jniEnv, jclass clazz, jlong ptr, jobject delegate) +{ + keyboard_hook* keyboard = (keyboard_hook*)ptr; + + if(keyboard->delegate) + { + jniEnv->DeleteGlobalRef(keyboard->delegate); + keyboard->delegate = NULL; + } + + if(delegate) + { + jobject delegate2 = jniEnv->NewGlobalRef(delegate); + if(delegate2) + { + jniEnv->GetJavaVM(&keyboard->jvm); + keyboard->delegate = delegate2; + } + } +} + +JNIEXPORT jboolean JNICALL Java_net_java_sip_communicator_impl_globalshortcut_NativeKeyboardHook_registerShortcut + (JNIEnv* jniEnv, jclass clazz, jlong ptr, jint keycode, jint modifiers) +{ + keyboard_hook* keyboard = (keyboard_hook*)ptr; + + if(keyboard && keyboard->hwnd) + { + int winModifiers = 0; + int winKeycode = 0; + keystrok ks; + + winKeycode = convertJavaKeycodeToWindows(keycode); + winModifiers = convertJavaUserDefinedModifiersToWindows(modifiers); + + ks.vkcode = winKeycode; + ks.modifiers = winModifiers; + ks.id = keyboard->hotkey_next_id; + ks.active = 0; + keyboard->keystrokes.push_back(ks); + keyboard->hotkey_next_id++; + PostMessage(keyboard->hwnd, WM_HOTKEY, 0, 0); + return true; + } + return false; +} + +JNIEXPORT void JNICALL Java_net_java_sip_communicator_impl_globalshortcut_NativeKeyboardHook_unregisterShortcut + (JNIEnv* jniEnv, jclass clazz, jlong ptr, jint keycode, jint modifiers) +{ + keyboard_hook* keyboard = (keyboard_hook*)ptr; + + if(keyboard && keyboard->hwnd != NULL) + { + int winModifiers = 0; + int winKeycode = 0; + + winKeycode = convertJavaKeycodeToWindows(keycode); + winModifiers = convertJavaUserDefinedModifiersToWindows(modifiers); + + for(std::list::iterator it = keyboard->keystrokes.begin() ; it != keyboard->keystrokes.end() ; ++it) + { + keystrok& ks = (*it); + if(ks.vkcode == winKeycode && ks.modifiers == winModifiers) + { + ks.active = -1; + PostMessage(keyboard->hwnd, WM_HOTKEY, 0, 0); + } + } + } +} + diff --git a/src/native/globalshortcut/net_java_sip_communicator_impl_globalshortcut_NativeKeyboardHook.h b/src/native/globalshortcut/net_java_sip_communicator_impl_globalshortcut_NativeKeyboardHook.h new file mode 100644 index 000000000..75025f931 --- /dev/null +++ b/src/native/globalshortcut/net_java_sip_communicator_impl_globalshortcut_NativeKeyboardHook.h @@ -0,0 +1,61 @@ +/* DO NOT EDIT THIS FILE - it is machine generated */ +#include +/* Header for class net_java_sip_communicator_impl_globalshortcut_NativeKeyboardHook */ + +#ifndef _Included_net_java_sip_communicator_impl_globalshortcut_NativeKeyboardHook +#define _Included_net_java_sip_communicator_impl_globalshortcut_NativeKeyboardHook +#ifdef __cplusplus +extern "C" { +#endif +/* + * Class: net_java_sip_communicator_impl_globalshortcut_NativeKeyboardHook + * Method: init + * Signature: ()J + */ +JNIEXPORT jlong JNICALL Java_net_java_sip_communicator_impl_globalshortcut_NativeKeyboardHook_init + (JNIEnv *, jclass); + +/* + * Class: net_java_sip_communicator_impl_globalshortcut_NativeKeyboardHook + * Method: start + * Signature: (J)V + */ +JNIEXPORT void JNICALL Java_net_java_sip_communicator_impl_globalshortcut_NativeKeyboardHook_start + (JNIEnv *, jclass, jlong); + +/* + * Class: net_java_sip_communicator_impl_globalshortcut_NativeKeyboardHook + * Method: stop + * Signature: (J)V + */ +JNIEXPORT void JNICALL Java_net_java_sip_communicator_impl_globalshortcut_NativeKeyboardHook_stop + (JNIEnv *, jclass, jlong); + +/* + * Class: net_java_sip_communicator_impl_globalshortcut_NativeKeyboardHook + * Method: setDelegate + * Signature: (JLnet/java/sip/communicator/impl/globalshortcut/NativeKeyboardHookDelegate;)V + */ +JNIEXPORT void JNICALL Java_net_java_sip_communicator_impl_globalshortcut_NativeKeyboardHook_setDelegate + (JNIEnv *, jclass, jlong, jobject); + +/* + * Class: net_java_sip_communicator_impl_globalshortcut_NativeKeyboardHook + * Method: registerShortcut + * Signature: (JII)Z + */ +JNIEXPORT jboolean JNICALL Java_net_java_sip_communicator_impl_globalshortcut_NativeKeyboardHook_registerShortcut + (JNIEnv *, jclass, jlong, jint, jint); + +/* + * Class: net_java_sip_communicator_impl_globalshortcut_NativeKeyboardHook + * Method: unregisterShortcut + * Signature: (JII)V + */ +JNIEXPORT void JNICALL Java_net_java_sip_communicator_impl_globalshortcut_NativeKeyboardHook_unregisterShortcut + (JNIEnv *, jclass, jlong, jint, jint); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/src/native/globalshortcut/net_java_sip_communicator_impl_globalshortcut_NativeKeyboardHook.m b/src/native/globalshortcut/net_java_sip_communicator_impl_globalshortcut_NativeKeyboardHook.m new file mode 100644 index 000000000..f46e3f9b7 --- /dev/null +++ b/src/native/globalshortcut/net_java_sip_communicator_impl_globalshortcut_NativeKeyboardHook.m @@ -0,0 +1,940 @@ +/* + * Jitsi Communicator, the OpenSource Java VoIP and Instant Messaging client. + * +/bin/bash: 2: command not found + * See terms of license at gnu.org. + */ + +#include "net_java_sip_communicator_impl_globalshortcut_NativeKeyboardHook.h" + +/* Mac OS X specific code */ + +#import +#import +#import + +#import "DDHotKeyCenter.h" +#import "javakey.h" + +/* following enum comes from http://snipplr.com/view/42797/ */ +enum +{ + kVK_ANSI_A = 0x00, + kVK_ANSI_S = 0x01, + kVK_ANSI_D = 0x02, + kVK_ANSI_F = 0x03, + kVK_ANSI_H = 0x04, + kVK_ANSI_G = 0x05, + kVK_ANSI_Z = 0x06, + kVK_ANSI_X = 0x07, + kVK_ANSI_C = 0x08, + kVK_ANSI_V = 0x09, + kVK_ANSI_B = 0x0B, + kVK_ANSI_Q = 0x0C, + kVK_ANSI_W = 0x0D, + kVK_ANSI_E = 0x0E, + kVK_ANSI_R = 0x0F, + kVK_ANSI_Y = 0x10, + kVK_ANSI_T = 0x11, + kVK_ANSI_1 = 0x12, + kVK_ANSI_2 = 0x13, + kVK_ANSI_3 = 0x14, + kVK_ANSI_4 = 0x15, + kVK_ANSI_6 = 0x16, + kVK_ANSI_5 = 0x17, + kVK_ANSI_Equal = 0x18, + kVK_ANSI_9 = 0x19, + kVK_ANSI_7 = 0x1A, + kVK_ANSI_Minus = 0x1B, + kVK_ANSI_8 = 0x1C, + kVK_ANSI_0 = 0x1D, + kVK_ANSI_RightBracket = 0x1E, + kVK_ANSI_O = 0x1F, + kVK_ANSI_U = 0x20, + kVK_ANSI_LeftBracket = 0x21, + kVK_ANSI_I = 0x22, + kVK_ANSI_P = 0x23, + kVK_ANSI_L = 0x25, + kVK_ANSI_J = 0x26, + kVK_ANSI_Quote = 0x27, + kVK_ANSI_K = 0x28, + kVK_ANSI_Semicolon = 0x29, + kVK_ANSI_Backslash = 0x2A, + kVK_ANSI_Comma = 0x2B, + kVK_ANSI_Slash = 0x2C, + kVK_ANSI_N = 0x2D, + kVK_ANSI_M = 0x2E, + kVK_ANSI_Period = 0x2F, + kVK_ANSI_Grave =JVK_2, + kVK_ANSI_KeypadDecimal = 0x41, + kVK_ANSI_KeypadMultiply = 0x43, + kVK_ANSI_KeypadPlus = 0x45, + kVK_ANSI_KeypadClear = 0x47, + kVK_ANSI_KeypadDivide = 0x4B, + kVK_ANSI_KeypadEnter = 0x4C, + kVK_ANSI_KeypadMinus = 0x4E, + kVK_ANSI_KeypadEquals = 0x51, + kVK_ANSI_Keypad0 = 0x52, + kVK_ANSI_Keypad1 = 0x53, + kVK_ANSI_Keypad2 = 0x54, + kVK_ANSI_Keypad3 = 0x55, + kVK_ANSI_Keypad4 = 0x56, + kVK_ANSI_Keypad5 = 0x57, + kVK_ANSI_Keypad6 = 0x58, + kVK_ANSI_Keypad7 = 0x59, + kVK_ANSI_Keypad8 = 0x5B, + kVK_ANSI_Keypad9 = 0x5C +}; + +/* keycodes for keys that are independent of keyboard layout*/ +enum +{ + kVK_Return = 0x24, + kVK_Tab =JVK_0, + kVK_Space =JVK_1, + kVK_Delete =JVK_3, + kVK_Escape =JVK_5, + kVK_Command =JVK_7, + kVK_Shift =JVK_8, + kVK_CapsLock =JVK_9, + kVK_Option =JVK_A, + kVK_Control =JVK_B, + kVK_RightShift =JVK_C, + kVK_RightOption =JVK_D, + kVK_RightControl =JVK_E, + kVK_Function =JVK_F, + kVK_F17 = 0x40, + kVK_VolumeUp = 0x48, + kVK_VolumeDown = 0x49, + kVK_Mute = 0x4A, + kVK_F18 = 0x4F, + kVK_F19 = 0x50, + kVK_F20 = 0x5A, + kVK_F5 = 0x60, + kVK_F6 = 0x61, + kVK_F7 = 0x62, + kVK_F3 = 0x63, + kVK_F8 = 0x64, + kVK_F9 = 0x65, + kVK_F11 = 0x67, + kVK_F13 = 0x69, + kVK_F16 = 0x6A, + kVK_F14 = 0x6B, + kVK_F10 = 0x6D, + kVK_F12 = 0x6F, + kVK_F15 = 0x71, + kVK_Help = 0x72, + kVK_Home = 0x73, + kVK_PageUp = 0x74, + kVK_ForwardDelete = 0x75, + kVK_F4 = 0x76, + kVK_End = 0x77, + kVK_F2 = 0x78, + kVK_PageDown = 0x79, + kVK_F1 = 0x7A, + kVK_LeftArrow = 0x7B, + kVK_RightArrow = 0x7C, + kVK_DownArrow = 0x7D, + kVK_UpArrow = 0x7E +}; + +/* ISO keyboards only*/ +enum +{ + kVK_ISO_Section = 0x0A +}; + +/* JIS keyboards only*/ +enum +{ + kVK_JIS_Yen = 0x5D, + kVK_JIS_Underscore = 0x5E, + kVK_JIS_KeypadComma = 0x5F, + kVK_JIS_Eisu = 0x66, + kVK_JIS_Kana = 0x68 +}; + +/** + * \brief Convert Java keycode to native ones. + * Reference: http://boredzo.org/blog/wp-content/uploads/2007/05/imtx-virtual-keycodes.png + * \param keycode Java keycode + * \return native Mac OS X keycode + */ +static int convertJavaKeycodeToMac(int keycode) +{ + int ret = -1; + switch(keycode) + { + /* 0 - 9 */ + case JVK_0: + ret = kVK_ANSI_0; + break; + case JVK_1: + ret = kVK_ANSI_1; + break; + case JVK_2: + ret = kVK_ANSI_2; + break; + case JVK_3: + ret = kVK_ANSI_3; + break; + case JVK_4: + ret = kVK_ANSI_4; + break; + case JVK_5: + ret = kVK_ANSI_5; + break; + case JVK_6: + ret = kVK_ANSI_6; + break; + case JVK_7: + ret = kVK_ANSI_7; + break; + case JVK_8: + ret = kVK_ANSI_8; + break; + case JVK_9: + ret = kVK_ANSI_9; + break; + /* A - Z */ + case JVK_A: + ret = kVK_ANSI_A; + break; + case JVK_B: + ret = kVK_ANSI_B; + break; + case JVK_C: + ret = kVK_ANSI_C; + break; + case JVK_D: + ret = kVK_ANSI_D; + break; + case JVK_E: + ret = kVK_ANSI_E; + break; + case JVK_F: + ret = kVK_ANSI_F; + break; + case JVK_G: + ret = kVK_ANSI_G; + break; + case JVK_H: + ret = kVK_ANSI_H; + break; + case JVK_I: + ret = kVK_ANSI_I; + break; + case JVK_J: + ret = kVK_ANSI_J; + break; + case JVK_K: + ret = kVK_ANSI_K; + break; + case JVK_L: + ret = kVK_ANSI_L; + break; + case JVK_M: + ret = kVK_ANSI_M; + break; + case JVK_N: + ret = kVK_ANSI_N; + break; + case JVK_O: + ret = kVK_ANSI_O; + break; + case JVK_P: + ret = kVK_ANSI_P; + break; + case JVK_Q: + ret = kVK_ANSI_Q; + break; + case JVK_R: + ret = kVK_ANSI_R; + break; + case JVK_S: + ret = kVK_ANSI_S; + break; + case JVK_T: + ret = kVK_ANSI_T; + break; + case JVK_U: + ret = kVK_ANSI_U; + break; + case JVK_V: + ret = kVK_ANSI_V; + break; + case JVK_W: + ret = kVK_ANSI_W; + break; + case JVK_X: + ret = kVK_ANSI_X; + break; + case JVK_Y: + ret = kVK_ANSI_Y; + break; + case JVK_Z: + ret = kVK_ANSI_Z; + break; + /* F1 - F12 */ + case JVK_F1: + ret = kVK_F1; + break; + case JVK_F2: + ret = kVK_F2; + break; + case JVK_F3: + ret = kVK_F3; + break; + case JVK_F4: + ret = kVK_F4; + break; + case JVK_F5: + ret = kVK_F5; + break; + case JVK_F6: + ret = kVK_F6; + break; + case JVK_F7: + ret = kVK_F7; + break; + case JVK_F8: + ret = kVK_F8; + break; + case JVK_F9: + ret = kVK_F9; + break; + case JVK_F10: + ret = kVK_F10; + break; + case JVK_F11: + ret = kVK_F11; + break; + case JVK_F12: + ret = kVK_F12; + break; + /* arrows (left, right, up, down) */ + case JVK_LEFT: + ret = kVK_LeftArrow; + break; + case JVK_RIGHT: + ret = kVK_RightArrow; + break; + case JVK_UP: + ret = kVK_UpArrow; + break; + case JVK_DOWN: + ret = kVK_DownArrow; + break; + case JVK_COMMA: + ret = kVK_ANSI_Comma; + break; + case JVK_MINUS: + ret = kVK_ANSI_Minus; + break; + case JVK_PLUS: + ret = kVK_ANSI_KeypadPlus; + break; + case JVK_PERIOD: + ret = kVK_ANSI_Period; + break; + case JVK_SLASH: + ret = kVK_ANSI_Slash; + break; + case JVK_BACK_SLASH: + ret = kVK_ANSI_Backslash; + break; + case JVK_SEMICOLON: + ret = kVK_ANSI_Semicolon; + break; + case JVK_EQUALS: + ret = kVK_ANSI_Equal; + break; + case JVK_COLON: + ret = -1; + break; + case JVK_UNDERSCORE: + ret = kVK_JIS_Underscore; + break; + case JVK_DOLLAR: + ret = -1; + break; + case JVK_EXCLAMATION_MARK: + ret = -1; + break; + case JVK_GREATER: + ret = -1; + break; + case JVK_LESS: + ret = -1; + break; + case JVK_QUOTE: + ret = kVK_ANSI_Quote; + break; + case JVK_BACK_QUOTE: + ret = -1; + break; + case JVK_INSERT: + ret = -1; + break; + case JVK_HELP: + ret = kVK_Help; + break; + case JVK_HOME: + ret = kVK_Home; + break; + case JVK_END: + ret = kVK_End; + break; + case JVK_PAGE_UP: + ret = kVK_PageUp; + break; + case JVK_PAGE_DOWN: + ret = kVK_PageDown; + break; + case JVK_OPEN_BRACKET: + ret = kVK_ANSI_LeftBracket; + break; + case JVK_CLOSE_BRACKET: + ret = kVK_ANSI_RightBracket; + break; + case 0x08: + ret = kVK_Delete; + break; + default: + break; + } + + return ret; +} + +/** + * \brief Convert Mac OS X keycode to Java ones. + * Reference: http://boredzo.org/blog/wp-content/uploads/2007/05/imtx-virtual-keycodes.png + * \param keycode Mac OS X keycode + * \return Java keycode + */ +static int convertMacKeycodeToJava(int keycode) +{ + int ret = -1; + switch(keycode) + { + /* 0 - 9 */ + case kVK_ANSI_0: + ret =JVK_0; + break; + case kVK_ANSI_1: + ret =JVK_1; + break; + case kVK_ANSI_2: + ret =JVK_2; + break; + case kVK_ANSI_3: + ret =JVK_3; + break; + case kVK_ANSI_4: + ret =JVK_4; + break; + case kVK_ANSI_5: + ret =JVK_5; + break; + case kVK_ANSI_6: + ret =JVK_6; + break; + case kVK_ANSI_7: + ret =JVK_7; + break; + case kVK_ANSI_8: + ret =JVK_8; + break; + case kVK_ANSI_9: + ret =JVK_9; + break; + /* A - Z */ + case kVK_ANSI_A: + ret =JVK_A; + break; + case kVK_ANSI_B: + ret =JVK_B; + break; + case kVK_ANSI_C: + ret =JVK_C; + break; + case kVK_ANSI_D: + ret =JVK_D; + break; + case kVK_ANSI_E: + ret =JVK_E; + break; + case kVK_ANSI_F: + ret =JVK_F; + break; + case kVK_ANSI_G: + ret =JVK_G; + break; + case kVK_ANSI_H: + ret =JVK_H; + break; + case kVK_ANSI_I: + ret =JVK_I; + break; + case kVK_ANSI_J: + ret =JVK_J; + break; + case kVK_ANSI_K: + ret =JVK_K; + break; + case kVK_ANSI_L: + ret =JVK_L; + break; + case kVK_ANSI_M: + ret =JVK_M; + break; + case kVK_ANSI_N: + ret =JVK_N; + break; + case kVK_ANSI_O: + ret =JVK_O; + break; + case kVK_ANSI_P: + ret =JVK_P; + break; + case kVK_ANSI_Q: + ret =JVK_Q; + break; + case kVK_ANSI_R: + ret =JVK_R; + break; + case kVK_ANSI_S: + ret =JVK_S; + break; + case kVK_ANSI_T: + ret =JVK_T; + break; + case kVK_ANSI_U: + ret =JVK_U; + break; + case kVK_ANSI_V: + ret =JVK_V; + break; + case kVK_ANSI_W: + ret =JVK_W; + break; + case kVK_ANSI_X: + ret =JVK_X; + break; + case kVK_ANSI_Y: + ret =JVK_Y; + break; + case kVK_ANSI_Z: + ret =JVK_Z; + break; + /* F1 - F12 */ + case kVK_F1: + ret =JVK_F1; + break; + case kVK_F2: + ret =JVK_F2; + break; + case kVK_F3: + ret =JVK_F3; + break; + case kVK_F4: + ret =JVK_F4; + break; + case kVK_F5: + ret =JVK_F5; + break; + case kVK_F6: + ret =JVK_F6; + break; + case kVK_F7: + ret =JVK_F7; + break; + case kVK_F8: + ret =JVK_F8; + break; + case kVK_F9: + ret =JVK_F9; + break; + case kVK_F10: + ret =JVK_F10; + break; + case kVK_F11: + ret =JVK_F11; + break; + case kVK_F12: + ret =JVK_F12; + break; + /* arrows (left, right, up, down) */ + case kVK_LeftArrow: + ret =JVK_LEFT; + break; + case kVK_RightArrow: + ret =JVK_RIGHT; + break; + case kVK_UpArrow: + ret =JVK_UP; + break; + case kVK_DownArrow: + ret =JVK_DOWN; + break; + case kVK_ANSI_Comma: + ret =JVK_COMMA; + break; + case kVK_ANSI_Minus: + ret =JVK_MINUS; + break; + case kVK_ANSI_KeypadPlus: + ret =JVK_PLUS; + break; + case kVK_ANSI_Period: + ret =JVK_PERIOD; + break; + case kVK_ANSI_Slash: + ret =JVK_SLASH; + break; + case kVK_ANSI_Backslash: + ret =JVK_BACK_SLASH; + break; + case kVK_ANSI_Semicolon: + ret =JVK_SEMICOLON; + break; + case kVK_ANSI_Equal: + ret =JVK_EQUALS; + break; + case kVK_JIS_Underscore: + ret =JVK_UNDERSCORE; + break; + case kVK_ANSI_Quote: + ret =JVK_QUOTE; + break; + case kVK_Help: + ret =JVK_HELP; + break; + case kVK_Home: + ret =JVK_HOME; + break; + case kVK_End: + ret =JVK_END; + break; + case kVK_PageUp: + ret =JVK_PAGE_UP; + break; + case kVK_PageDown: + ret =JVK_PAGE_DOWN; + break; + case kVK_ANSI_LeftBracket: + ret =JVK_OPEN_BRACKET; + break; + case kVK_ANSI_RightBracket: + ret =JVK_CLOSE_BRACKET; + break; + case kVK_Delete: + ret = 0x08; + break; + default: + break; + } + return ret; +} + +/** + * \brief Convert Java user-defined modifiers to Mac OS X ones. + * \param modifiers Java user-defined modifiers + * \return Mac OS X modifiers + */ +static int convertJavaUserDefinedModifiersToMac(int modifiers) +{ + int macModifiers = 0; + + if(modifiers & 0x01) + { + /* CTRL */ + macModifiers |= NSControlKeyMask; + } + if(modifiers & 0x02) + { + /* ALT */ + macModifiers |= NSAlternateKeyMask; + } + if(modifiers & 0x04) + { + /* SHIFT */ + macModifiers |= NSShiftKeyMask; + } + if(modifiers & 0x08) + { + /* LOGO */ + macModifiers |= NSCommandKeyMask; + } + + return macModifiers; +} + +/** + * \brief Convert Mac modifiers to our Java user-defined ones. + * \param modifiers Mac modifiers (MOD_CONTROL, ...) + * \return Java user-defined modifiers + */ +static int convertMacModifiersToJavaUserDefined(int modifiers) +{ + int javaModifiers = 0; + + if(modifiers & NSControlKeyMask) + { + javaModifiers |= 0x01; + } + if(modifiers & NSAlternateKeyMask) + { + javaModifiers |= 0x02; + } + if(modifiers & NSShiftKeyMask) + { + javaModifiers |= 0x04; + } + if(modifiers & NSCommandKeyMask) + { + javaModifiers |= 0x08; + } + + return javaModifiers; +} + +@interface KeyboardHook: NSObject +{ + @private + jobject delegateObject; + JavaVM* vm; +} + +-(void)dealloc; +-(id)init; +-(void) setDelegate:(jobject)delegate inJNIEnv:(JNIEnv* )jniEnv; +-(void) hotkeyAction:(NSEvent*)hotKeyEvent inObject:(id)anObject; +-(void) notify:(int) keyCode inModifiers:(int)modifiers; +-(int) registerKey:(int)keycode inModifiers:(int)modifiers; +-(void) unregisterKey:(int)keycode inModifiers:(int)modifiers; +@end + +@implementation KeyboardHook +-(void) notify:(int) keyCode inModifiers:(int)modifiers; +{ + jobject delegate; + JNIEnv* jniEnv = NULL; + jclass delegateClass = NULL; + + delegate = self->delegateObject; + if(!delegate) + return; + + vm = self->vm; + if(0 != (*vm)->AttachCurrentThreadAsDaemon(vm, (void**)&jniEnv, NULL)) + return; + + delegateClass = (*jniEnv)->GetObjectClass(jniEnv, delegate); + if(delegateClass) + { + jmethodID methodid = NULL; + + methodid = (*jniEnv)->GetMethodID(jniEnv, delegateClass,"receiveKey", "(II)V"); + if(methodid) + { + (*jniEnv)->CallVoidMethod(jniEnv, delegate, methodid, keyCode, modifiers); + } + } + (*jniEnv)->ExceptionClear(jniEnv); +} + +- (void)setDelegate:(jobject) delegate inJNIEnv:(JNIEnv*)jniEnv +{ + if(self->delegateObject) + { + if(!jniEnv) + (*(self->vm))->AttachCurrentThread(self->vm, (void**)&jniEnv, NULL); + (*jniEnv)->DeleteGlobalRef(jniEnv, self->delegateObject); + self->delegateObject = NULL; + self->vm = NULL; + } + + if(delegate) + { + delegate = (*jniEnv)->NewGlobalRef(jniEnv, delegate); + if(delegate) + { + (*jniEnv)->GetJavaVM(jniEnv, &(self->vm)); + self->delegateObject = delegate; + } + } +} + +-(void) hotkeyAction:(NSEvent*)hotKeyEvent inObject:(id)anObject; +{ + int modifiers = [hotKeyEvent modifierFlags]; + int keycode = [hotKeyEvent keyCode]; + int javaModifiers = 0; + int javaKeycode = 0; + + (void)anObject; + + javaKeycode = convertMacKeycodeToJava(keycode); + javaModifiers = convertMacModifiersToJavaUserDefined(modifiers); + + [self notify:javaKeycode inModifiers:javaModifiers]; +} + +- (id)init +{ + if((self = [super init])) + { + self->delegateObject = NULL; + self->vm = NULL; + } + return self; +} + +- (void)dealloc +{ + [self setDelegate:NULL inJNIEnv:NULL]; + [super dealloc]; +} + +- (int) registerKey:(int)keycode inModifiers:(int)modifiers +{ + NSAutoreleasePool* autoreleasePool = NULL; + DDHotKeyCenter* c = NULL; + + autoreleasePool = [[NSAutoreleasePool alloc] init]; + c = [[DDHotKeyCenter alloc] init]; + + /* + DDHotKeyTask task = ^(NSEvent* hkEvent) + { + printf("hot task\n");fflush(stdout); + }; + */ + + if(![c registerHotKeyWithKeyCode:keycode modifierFlags:(modifiers) target:self action:@selector(hotkeyAction:inObject:) object:nil]) + { + [c release]; + [autoreleasePool release]; + return 0; + } + [c release]; + [autoreleasePool release]; + return 1; +} + +- (void) unregisterKey:(int)keycode inModifiers:(int)modifiers +{ + NSAutoreleasePool* autoreleasePool; + DDHotKeyCenter* c = NULL; + + autoreleasePool = [[NSAutoreleasePool alloc] init]; + c = [[DDHotKeyCenter alloc] init]; + + [c unregisterHotKeyWithKeyCode:keycode modifierFlags:(modifiers)]; + [c release]; + [autoreleasePool release]; +} +@end + + JNIEXPORT jlong JNICALL Java_net_java_sip_communicator_impl_globalshortcut_NativeKeyboardHook_init +(JNIEnv* jniEnv, jclass clazz) +{ + KeyboardHook* keyboard = NULL; + NSAutoreleasePool* autoreleasePool; + + (void)jniEnv; + (void)clazz; + + autoreleasePool = [[NSAutoreleasePool alloc] init]; + keyboard = [[KeyboardHook alloc] init]; + + [autoreleasePool release]; + + return (jlong)keyboard; +} + +JNIEXPORT void JNICALL Java_net_java_sip_communicator_impl_globalshortcut_NativeKeyboardHook_start + (JNIEnv* jniEnv, jclass clazz, jlong ptr) +{ + (void)jniEnv; + (void)clazz; + (void)ptr; + /* do nothing */ +} + +JNIEXPORT void JNICALL Java_net_java_sip_communicator_impl_globalshortcut_NativeKeyboardHook_stop + (JNIEnv* jniEnv, jclass clazz, jlong ptr) +{ + (void)jniEnv; + (void)clazz; + (void)ptr; + /* do nothing */ +} + +JNIEXPORT void JNICALL Java_net_java_sip_communicator_impl_globalshortcut_NativeKeyboardHook_setDelegate + (JNIEnv* jniEnv, jclass clazz, jlong ptr, jobject delegate) +{ + KeyboardHook* keyboard = NULL; + + (void)clazz; + + if(delegate) + { + keyboard = (KeyboardHook*)ptr; + [keyboard setDelegate:delegate inJNIEnv:jniEnv]; + } + else + { + keyboard = NULL; + } +} + +JNIEXPORT jboolean JNICALL Java_net_java_sip_communicator_impl_globalshortcut_NativeKeyboardHook_registerShortcut + (JNIEnv* jniEnv, jclass clazz, jlong ptr, jint keycode, jint modifiers) +{ + KeyboardHook* keyboard = (KeyboardHook*)ptr; + + (void)jniEnv; + (void)clazz; + + if(keyboard) + { + int macKeycode = convertJavaKeycodeToMac(keycode); + int macModifiers = convertJavaUserDefinedModifiersToMac(modifiers); + if(macKeycode == -1) + { + return JNI_FALSE; + } + + if([keyboard registerKey:macKeycode inModifiers:macModifiers]) + { + return JNI_TRUE; + } + } + return JNI_FALSE; +} + +JNIEXPORT void JNICALL Java_net_java_sip_communicator_impl_globalshortcut_NativeKeyboardHook_unregisterShortcut + (JNIEnv* jniEnv, jclass clazz, jlong ptr, jint keycode, jint modifiers) +{ + KeyboardHook* keyboard = (KeyboardHook*)ptr; + + (void)jniEnv; + (void)clazz; + + if(keyboard) + { + int macKeycode = convertJavaKeycodeToMac(keycode); + int macModifiers = convertJavaUserDefinedModifiersToMac(modifiers); + + if(macModifiers == -1) + { + return; + } + + [keyboard unregisterKey:macKeycode inModifiers:macModifiers]; + } +} + diff --git a/src/net/java/sip/communicator/impl/configuration/HashtableConfigurationStore.java b/src/net/java/sip/communicator/impl/configuration/HashtableConfigurationStore.java index 6cd39644e..4ab7a738c 100644 --- a/src/net/java/sip/communicator/impl/configuration/HashtableConfigurationStore.java +++ b/src/net/java/sip/communicator/impl/configuration/HashtableConfigurationStore.java @@ -10,6 +10,7 @@ /** * + * @param the hashtable extension * @author Lyubomir Marinov */ public abstract class HashtableConfigurationStore diff --git a/src/net/java/sip/communicator/impl/globalshortcut/CallShortcut.java b/src/net/java/sip/communicator/impl/globalshortcut/CallShortcut.java new file mode 100644 index 000000000..c6a17ed3b --- /dev/null +++ b/src/net/java/sip/communicator/impl/globalshortcut/CallShortcut.java @@ -0,0 +1,195 @@ +/* + * Jitsi, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. + * See terms of license at gnu.org. + */ +package net.java.sip.communicator.impl.globalshortcut; + +import java.awt.*; +import java.util.*; +import java.util.List; // disambiguation + +import net.java.sip.communicator.service.globalshortcut.*; +import net.java.sip.communicator.service.keybindings.*; +import net.java.sip.communicator.service.protocol.*; +import net.java.sip.communicator.service.protocol.event.*; +import net.java.sip.communicator.util.*; + +/** + * Shortcut for call (take the call, hang up, ...). + * + * @author Sebastien Vincent + */ +public class CallShortcut + implements GlobalShortcutListener, + CallListener +{ + /** + * The Logger used by the CallShortcut class + * and its instances for logging output. + */ + private static final Logger logger = Logger.getLogger(CallShortcut.class); + + /** + * Keybindings service. + */ + private KeybindingsService keybindingsService = + GlobalShortcutActivator.getKeybindingsService(); + + /** + * List of incoming calls. + */ + private ArrayList incomingCalls = new ArrayList(); + + /** + * Constructor. + */ + public CallShortcut() + { + } + + /** + * Callback when an shortcut is typed + * + * @param evt GlobalShortcutEvent + */ + public void shortcutReceived(GlobalShortcutEvent evt) + { + AWTKeyStroke keystroke = evt.getKeyStroke(); + GlobalKeybindingSet set = keybindingsService.getGlobalBindings(); + + Call choosenCall = null; + + for(Map.Entry> entry : + set.getBindings().entrySet()) + { + for(AWTKeyStroke ks : entry.getValue()) + { + if(entry.getKey().equals("answer") && + keystroke.getKeyCode() == ks.getKeyCode() && + keystroke.getModifiers() == ks.getModifiers()) + { + synchronized(incomingCalls) + { + int size = incomingCalls.size(); + + for(int i = 0 ; i < size ; i++) + { + Call c = incomingCalls.get(i); + + if(c.getCallPeers().next().getState() == + CallPeerState.INCOMING_CALL) + { + choosenCall = c; + break; + } + } + } + + if(choosenCall == null) + return; + + final OperationSetBasicTelephony opSet = + choosenCall.getProtocolProvider().getOperationSet( + OperationSetBasicTelephony.class); + final Call cCall = choosenCall; + + new Thread() + { + public void run() + { + try + { + opSet.answerCallPeer(cCall.getCallPeers().next()); + } + catch(OperationFailedException e) + { + logger.info("Failed to answer call via global shortcut", e); + } + } + }.start(); + } + else if(entry.getKey().equals("hangup") && + keystroke.getKeyCode() == ks.getKeyCode() && + keystroke.getModifiers() == ks.getModifiers()) + { + Call incomingCall = null; + + synchronized(incomingCalls) + { + int size = incomingCalls.size(); + + for(int i = 0 ; i < size ; i++) + { + Call c = incomingCalls.get(i); + + if(c.getCallPeers().next().getState() == + CallPeerState.CONNECTED) + { + choosenCall = c; + break; + } + else if(c.getCallPeers().next().getState() == + CallPeerState.INCOMING_CALL && incomingCall == null) + { + incomingCall = c; + } + } + } + + if(choosenCall == null && incomingCall != null) + { + // maybe we just want to hangup (refuse) incoming call + choosenCall = incomingCall; + } + + if(choosenCall == null) + return; + + final OperationSetBasicTelephony opSet = + choosenCall.getProtocolProvider().getOperationSet( + OperationSetBasicTelephony.class); + + final Call cCall = choosenCall; + new Thread() + { + public void run() + { + try + { + + opSet.hangupCallPeer(cCall.getCallPeers().next()); + } + catch(OperationFailedException e) + { + logger.info("Failed to answer call via global shortcut", e); + } + } + }.start(); + } + } + } + } + + public void incomingCallReceived(CallEvent event) + { + Call sourceCall = event.getSourceCall(); + + synchronized(incomingCalls) + { + incomingCalls.add(sourceCall); + } + } + + public void outgoingCallCreated(CallEvent event) + { + /* do nothing */ + } + + public void callEnded(CallEvent event) + { + Call sourceCall = event.getSourceCall(); + incomingCalls.remove(sourceCall); + } +} diff --git a/src/net/java/sip/communicator/impl/globalshortcut/GlobalShortcutActivator.java b/src/net/java/sip/communicator/impl/globalshortcut/GlobalShortcutActivator.java new file mode 100644 index 000000000..998e7ab80 --- /dev/null +++ b/src/net/java/sip/communicator/impl/globalshortcut/GlobalShortcutActivator.java @@ -0,0 +1,241 @@ +/* + * Jitsi, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. + * See terms of license at gnu.org. + */ +package net.java.sip.communicator.impl.globalshortcut; + +import net.java.sip.communicator.service.globalshortcut.*; +import net.java.sip.communicator.service.gui.*; +import net.java.sip.communicator.service.keybindings.*; +import net.java.sip.communicator.service.protocol.*; +import net.java.sip.communicator.util.*; + +import org.osgi.framework.*; + +/** + * OSGi Activator for global shortcut. + * + * @author Sebastien Vincent + */ +public class GlobalShortcutActivator + implements BundleActivator +{ + /** + * The Logger used by the GlobalShortcutActivator class + * and its instances for logging output. + */ + private static final Logger logger = + Logger.getLogger(GlobalShortcutActivator.class); + + /** + * The OSGi ServiceRegistration of GlobalShortcut. + */ + private ServiceRegistration serviceRegistration; + + /** + * The GlobalShortcutServiceImpl. + */ + protected static GlobalShortcutServiceImpl globalShortcutService = null; + + /** + * OSGi bundle context. + */ + private static BundleContext bundleContext = null; + + /** + * Keybindings service reference. + */ + private static KeybindingsService keybindingsService = null; + + /** + * UI service reference. + */ + private static UIService uiService = null; + + /** + * Returns the KeybindingsService obtained from the bundle context. + * + * @return the KeybindingsService obtained from the bundle context + */ + public static KeybindingsService getKeybindingsService() + { + if (keybindingsService == null) + { + keybindingsService + = ServiceUtils.getService( + bundleContext, + KeybindingsService.class); + } + return keybindingsService; + } + + /** + * Returns the UIService obtained from the bundle context. + * + * @return the UIService obtained from the bundle context + */ + public static UIService getUIService() + { + if (uiService == null) + { + uiService + = ServiceUtils.getService( + bundleContext, + UIService.class); + } + return uiService; + } + + /** + * Starts the execution of this service bundle in the specified context. + * + * @param bundleContext the context in which the service bundle is to + * start executing + * @throws Exception if an error occurs while starting the execution of the + * service bundle in the specified context + */ + public void start(BundleContext bundleContext) + throws Exception + { + GlobalShortcutActivator.bundleContext = bundleContext; + serviceRegistration = null; + globalShortcutService = new GlobalShortcutServiceImpl(); + globalShortcutService.start(); + bundleContext.registerService(GlobalShortcutService.class.getName(), + globalShortcutService, null); + + globalShortcutService.reloadGlobalShortcuts(); + + registerListenerWithProtocolProviderService(); + + bundleContext.addServiceListener(new ServiceListener() + { + public void serviceChanged(ServiceEvent serviceEvent) + { + GlobalShortcutActivator.this.serviceChanged(serviceEvent); + } + }); + if (logger.isDebugEnabled()) + logger.debug("GlobalShortcut Service ... [REGISTERED]"); + } + + /** + * Stops the execution of this service bundle in the specified context. + * + * @param bundleContext the context in which this service bundle is to + * stop executing + * @throws Exception if an error occurs while stopping the execution of the + * service bundle in the specified context + */ + public void stop(BundleContext bundleContext) + throws Exception + { + if (serviceRegistration != null) + { + globalShortcutService.stop(); + serviceRegistration.unregister(); + serviceRegistration = null; + + if (logger.isDebugEnabled()) + logger.debug("GlobalShortcut Service ... [UNREGISTERED]"); + } + + bundleContext = null; + } + + /** + * Implements the ServiceListener method. Verifies whether the + * passed event concerns a ProtocolProviderService and adds the + * corresponding UI controls. + * + * @param event The ServiceEvent object. + */ + private void serviceChanged(ServiceEvent event) + { + ServiceReference serviceRef = event.getServiceReference(); + + // if the event is caused by a bundle being stopped, we don't want to + // know + if (serviceRef.getBundle().getState() == Bundle.STOPPING) + { + return; + } + + Object service = bundleContext.getService(serviceRef); + + // we don't care if the source service is not a protocol provider + if (!(service instanceof ProtocolProviderService)) + { + return; + } + + switch (event.getType()) + { + case ServiceEvent.REGISTERED: + this.handleProviderAdded((ProtocolProviderService) service); + break; + case ServiceEvent.UNREGISTERING: + this.handleProviderRemoved((ProtocolProviderService) service); + break; + } + } + + /** + * Get all registered ProtocolProviderService and set our listener. + */ + public void registerListenerWithProtocolProviderService() + { + ServiceReference refs[] = null; + try + { + refs = bundleContext.getServiceReferences( + ProtocolProviderService.class.getName(), null); + } + catch (InvalidSyntaxException e) + { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + for(ServiceReference ref : refs) + { + ProtocolProviderService service = + (ProtocolProviderService)bundleContext.getService(ref); + + OperationSetBasicTelephony opSet = + service.getOperationSet(OperationSetBasicTelephony.class); + + opSet.addCallListener(globalShortcutService.getCallShortcut()); + } + } + + /** + * Notifies this manager that a specific + * ProtocolProviderService has been registered as a service. + * + * @param provider the ProtocolProviderService which has been + * registered as a service. + */ + private void handleProviderAdded(final ProtocolProviderService provider) + { + OperationSetBasicTelephony opSet = + provider.getOperationSet(OperationSetBasicTelephony.class); + opSet.addCallListener(globalShortcutService.getCallShortcut()); + } + + /** + * Notifies this manager that a specific + * ProtocolProviderService has been unregistered as a service. + * + * @param provider the ProtocolProviderService which has been + * unregistered as a service. + */ + private void handleProviderRemoved(ProtocolProviderService provider) + { + OperationSetBasicTelephony opSet = + provider.getOperationSet(OperationSetBasicTelephony.class); + opSet.removeCallListener(globalShortcutService.getCallShortcut()); + } +} diff --git a/src/net/java/sip/communicator/impl/globalshortcut/GlobalShortcutServiceImpl.java b/src/net/java/sip/communicator/impl/globalshortcut/GlobalShortcutServiceImpl.java new file mode 100644 index 000000000..c74746c1e --- /dev/null +++ b/src/net/java/sip/communicator/impl/globalshortcut/GlobalShortcutServiceImpl.java @@ -0,0 +1,358 @@ +/* + * Jitsi, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. + * See terms of license at gnu.org. + */ +package net.java.sip.communicator.impl.globalshortcut; + +import java.util.*; +import java.util.List; // disambiguation + +import java.awt.*; + +import net.java.sip.communicator.service.globalshortcut.*; +import net.java.sip.communicator.service.keybindings.*; +import net.java.sip.communicator.util.*; + +/** + * This global shortcut service permits to register listeners for global + * shortcut (i.e. keystroke even if application is not foreground). + * + * @author Sebastien Vincent + */ +public class GlobalShortcutServiceImpl + implements GlobalShortcutService, + NativeKeyboardHookDelegate +{ + /** + * The Logger used by the GlobalShortcutServiceImpl class + * and its instances for logging output. + */ + private static final Logger logger = Logger.getLogger( + GlobalShortcutServiceImpl.class); + + /** + * List of action and its corresponding shortcut. + */ + private final Map> mapActions = + new HashMap>(); + + /** + * If the service is running or not. + */ + private boolean isRunning = false; + + /** + * The NativeKeyboardHook that will notify us key press event. + */ + private NativeKeyboardHook keyboardHook = new NativeKeyboardHook(); + + /** + * Call shortcut to answer/hang up a call. + */ + private final CallShortcut callShortcut = new CallShortcut(); + + /** + * UI shortcut to display GUI. + */ + private final UIShortcut uiShortcut = new UIShortcut(); + + /** + * Initializes the GlobalShortcutServiceImpl. + */ + public GlobalShortcutServiceImpl() + { + } + + /** + * Registers an action to execute when the keystroke is typed. + * + * @param listener listener to notify when keystroke is typed + * @param keyStroke keystroke that will trigger the action + */ + public void registerShortcut(GlobalShortcutListener listener, + AWTKeyStroke keyStroke) + { + List keystrokes = mapActions.get(listener); + + if(keyStroke == null) + { + return; + } + + if(keystrokes != null) + { + if(keyboardHook.registerShortcut(keyStroke.getKeyCode(), + getModifiers(keyStroke))) + { + keystrokes.add(keyStroke); + } + } + else + { + keystrokes = new ArrayList(); + if(keyboardHook.registerShortcut(keyStroke.getKeyCode(), + getModifiers(keyStroke))) + { + keystrokes.add(keyStroke); + } + } + + synchronized(mapActions) + { + mapActions.put(listener, keystrokes); + } + } + + /** + * Unregisters an action to execute when the keystroke is typed. + * + * @param listener listener to remove + * @param keyStroke keystroke that will trigger the action + */ + public void unregisterShortcut(GlobalShortcutListener listener, + AWTKeyStroke keyStroke) + { + List keystrokes = mapActions.get(listener); + + if(keystrokes != null && keyStroke != null) + { + int keycode = keyStroke.getKeyCode(); + int modifiers = keyStroke.getModifiers(); + AWTKeyStroke ks = null; + + for(AWTKeyStroke l : keystrokes) + { + if(l.getKeyCode() == keycode && l.getModifiers() == modifiers) + ks = l; + } + + if(ks != null) + { + keystrokes.remove(ks); + } + + keyboardHook.unregisterShortcut(keyStroke.getKeyCode(), + getModifiers(keyStroke)); + + synchronized(mapActions) + { + if(keystrokes.size() == 0) + { + mapActions.remove(listener); + } + else + { + mapActions.put(listener, keystrokes); + } + } + } + } + + /** + * Start the service. + */ + public void start() + { + if(!isRunning) + { + keyboardHook.setDelegate(this); + keyboardHook.start(); + isRunning = true; + } + } + + /** + * Stop the service. + */ + public void stop() + { + Collection> lst = mapActions.values(); + + isRunning = false; + + for(List kss : lst) + { + for(AWTKeyStroke e : kss) + { + unregisterShortcut(callShortcut, e); + unregisterShortcut(uiShortcut, e); + } + } + + if(keyboardHook != null) + { + keyboardHook.setDelegate(null); + keyboardHook.stop(); + } + } + + /** + * Receive a key press event. + * + * @param keycode keycode received + * @param modifiers modifiers received (ALT or CTRL + letter, ...) + */ + public void receiveKey(int keycode, int modifiers) + { + synchronized(mapActions) + { + // compare keycode/modifiers to keystroke + for(Map.Entry> entry : + mapActions.entrySet()) + { + List lst = entry.getValue(); + + for(AWTKeyStroke l : lst) + { + if(l.getKeyCode() == keycode && + getModifiers(l) == modifiers) + { + // notify corresponding listeners + GlobalShortcutEvent evt = new GlobalShortcutEvent(l); + entry.getKey().shortcutReceived(evt); + return; + } + } + } + } + } + + /** + * Get our user-defined modifiers. + * + * @param keystroke keystroke + * @return user-defined modifiers + */ + private static int getModifiers(AWTKeyStroke keystroke) + { + int modifiers = keystroke.getModifiers(); + int ret = 0; + + if((modifiers & java.awt.event.InputEvent.CTRL_DOWN_MASK) > 0) + { + ret |= NativeKeyboardHookDelegate.MODIFIERS_CTRL; + } + if((modifiers & java.awt.event.InputEvent.ALT_DOWN_MASK) > 0) + { + ret |= NativeKeyboardHookDelegate.MODIFIERS_ALT; + } + if((modifiers & java.awt.event.InputEvent.SHIFT_DOWN_MASK) > 0) + { + ret |= NativeKeyboardHookDelegate.MODIFIERS_SHIFT; + } + if((modifiers & java.awt.event.InputEvent.META_DOWN_MASK) > 0) + { + ret |= NativeKeyboardHookDelegate.MODIFIERS_LOGO; + } + + return ret; + } + + + /** + * Reload global shortcuts. + */ + public synchronized void reloadGlobalShortcuts() + { + // unregister all shortcuts + GlobalKeybindingSet set = + GlobalShortcutActivator.getKeybindingsService().getGlobalBindings(); + + for(Map.Entry> entry : + set.getBindings().entrySet()) + { + for(AWTKeyStroke e : entry.getValue()) + { + unregisterShortcut(callShortcut, e); + unregisterShortcut(uiShortcut, e); + } + } + + // add shortcuts from configuration + for(Map.Entry> entry : + set.getBindings().entrySet()) + { + if(entry.getKey().equals("answer") || + entry.getKey().equals("hangup")) + { + for(AWTKeyStroke e : entry.getValue()) + { + registerShortcut(callShortcut, e); + } + } + else if(entry.getKey().equals("contactlist")) + { + for(AWTKeyStroke e : entry.getValue()) + { + registerShortcut(uiShortcut, e); + } + } + } + } + + /** + * Returns CallShortcut object. + * + * @return CallShortcut object + */ + public CallShortcut getCallShortcut() + { + return callShortcut; + } + + /** + * Returns UIShortcut object. + * + * @return UIShortcut object + */ + public UIShortcut getUIShortcut() + { + return uiShortcut; + } + + /** + * Simple test. + */ + public void test() + { + GlobalShortcutListener l = new GlobalShortcutListener() + { + public void shortcutReceived(GlobalShortcutEvent evt) + { + System.out.println("global shortcut event"); + } + }; + + AWTKeyStroke ks = AWTKeyStroke.getAWTKeyStroke("control B"); + AWTKeyStroke ks2 = AWTKeyStroke.getAWTKeyStroke("control E"); + + if(ks == null) + { + logger.info("Failed to register keystroke"); + System.out.println("failed to register keystroke"); + return; + } + + this.registerShortcut(l, ks); + this.registerShortcut(l, ks2); + try{Thread.sleep(30000);}catch(InterruptedException e){} + this.unregisterShortcut(l, ks); + try{Thread.sleep(5000);}catch(InterruptedException e){} + this.unregisterShortcut(l, ks2); + + /* + boolean ret = keyboardHook.registerShortcut(ks.getKeyCode(), + getModifiers(ks)); + System.out.println("finally " + ret); + + System.out.println("registered"); + try{Thread.sleep(30000);}catch(InterruptedException e){} + System.out.println("unregistered1"); + keyboardHook.unregisterShortcut(ks.getKeyCode(), + getModifiers(ks)); + System.out.println("unregistered2"); + */ + } +} diff --git a/src/net/java/sip/communicator/impl/globalshortcut/NativeKeyboardHook.java b/src/net/java/sip/communicator/impl/globalshortcut/NativeKeyboardHook.java new file mode 100644 index 000000000..e2101c4e9 --- /dev/null +++ b/src/net/java/sip/communicator/impl/globalshortcut/NativeKeyboardHook.java @@ -0,0 +1,171 @@ +/* + * Jitsi, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. + * See terms of license at gnu.org. + */ +package net.java.sip.communicator.impl.globalshortcut; + +import net.java.sip.communicator.util.*; + +/** + * Native hook for keyboard. It is used to notify a + * NativeKeyboardHookDelegate for key (even if key are pressed when + * application is not in foreground). + * + * @author Sebastien Vincent + */ +public class NativeKeyboardHook +{ + /** + * The Logger used by the NativeKeyboardHook class and its + * instances for logging output. + */ + private static final Logger logger = Logger.getLogger( + NativeKeyboardHook.class); + + /** + * If it is started. + */ + private boolean isStarted = false; + + /** + * Native pointer. + */ + private static long ptr = 0; + + /** + * Constructor. + */ + public NativeKeyboardHook() + { + } + + /** + * Start the NativeKeyboardHook. + */ + public synchronized void start() + { + if(!isStarted && ptr != 0) + { + isStarted = true; + start(ptr); + } + } + + /** + * Stop the NativeKeyboardHook. + */ + public synchronized void stop() + { + if(isStarted && ptr != 0) + { + isStarted = false; + stop(ptr); + } + } + + /** + * Set delegate object for event notification. + * + * @param delegate delegate object + */ + public synchronized void setDelegate(NativeKeyboardHookDelegate delegate) + { + if(ptr != 0) + setDelegate(ptr, delegate); + } + + /** + * Register a shortcut. + * + * @param keycode keycode of the shortcut + * @param modifiers modifiers (CTRL, ALT, ...) + * @return true if success, false otherwise + */ + public synchronized boolean registerShortcut(int keycode, + int modifiers) + { + if(ptr != 0) + return registerShortcut(ptr, keycode, modifiers); + + return false; + } + + /** + * Unregister a shortcut. + * + * @param keycode keycode of the shortcut + * @param modifiers modifiers (CTRL, ALT, ...) + */ + public synchronized void unregisterShortcut(int keycode, + int modifiers) + { + if(ptr != 0) + unregisterShortcut(ptr, keycode, modifiers); + } + + /** + * Native method to initialize NativeKeyboardHook. + * + * @return native pointer. + */ + private static native long init(); + + /** + * Native method to start NativeKeyboardHook. + * + * @param ptr native pointer + */ + private static native void start(long ptr); + + /** + * Native method to stop NativeKeyboardHook. + * + * @param ptr native pointer + */ + private static native void stop(long ptr); + + /** + * Native method to set the delegate object. + * + * @param ptr native pointer + * @param delegate delegate object to set + */ + private static native void setDelegate(long ptr, + NativeKeyboardHookDelegate delegate); + + /** + * Native method to register a shortcut. + * + * @param ptr native pointer + * @param keycode keycode of the shortcut + * @param modifiers modifiers (CTRL, ALT, ...) + */ + private static native boolean registerShortcut(long ptr, int keycode, + int modifiers); + + /** + * Native method to unregister a shortcut. + * + * @param ptr native pointer + * @param keycode keycode of the shortcut + * @param modifiers modifiers (CTRL, ALT, ...) + */ + private static native void unregisterShortcut(long ptr, int keycode, + int modifiers); + + static + { + try + { + System.loadLibrary("globalshortcut"); + ptr = init(); + } + catch(Exception e) + { + logger.warn("Failed to load globalshortcut", e); + ptr = 0; + } + } +} diff --git a/src/net/java/sip/communicator/impl/globalshortcut/NativeKeyboardHookDelegate.java b/src/net/java/sip/communicator/impl/globalshortcut/NativeKeyboardHookDelegate.java new file mode 100644 index 000000000..04b135c32 --- /dev/null +++ b/src/net/java/sip/communicator/impl/globalshortcut/NativeKeyboardHookDelegate.java @@ -0,0 +1,44 @@ +/* + * Jitsi, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. + * See terms of license at gnu.org. + */ +package net.java.sip.communicator.impl.globalshortcut; + +/** + * NativeKeyboardHookDelegate interface. + * + * @author Sebastien Vincent + */ +public interface NativeKeyboardHookDelegate +{ + /** + * CTRL modifier. + */ + public static final int MODIFIERS_CTRL = 1; + + /** + * ALT modifier. + */ + public static final int MODIFIERS_ALT = 2; + + /** + * SHIFT modifier. + */ + public static final int MODIFIERS_SHIFT = 4; + + /** + * Logo modifier (i.e. CMD/Apple key on Mac OS X, Windows key on + * MS Windows). + */ + public static final int MODIFIERS_LOGO = 8; + + /** + * Receive a key press event. + * + * @param keycode keycode received + * @param modifiers modifiers received (ALT or CTRL + letter, ...) + */ + public void receiveKey(int keycode, int modifiers); +} \ No newline at end of file diff --git a/src/net/java/sip/communicator/impl/globalshortcut/UIShortcut.java b/src/net/java/sip/communicator/impl/globalshortcut/UIShortcut.java new file mode 100644 index 000000000..cf6aa0be8 --- /dev/null +++ b/src/net/java/sip/communicator/impl/globalshortcut/UIShortcut.java @@ -0,0 +1,68 @@ +/* + * Jitsi, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. + * See terms of license at gnu.org. + */ +package net.java.sip.communicator.impl.globalshortcut; + +import java.awt.*; +import java.util.*; +import java.util.List; // disambiguation + +import net.java.sip.communicator.service.globalshortcut.*; +import net.java.sip.communicator.service.gui.*; +import net.java.sip.communicator.service.keybindings.*; + +/** + * UI shortcut. + * + * @author Sebastien Vincent + */ +public class UIShortcut + implements GlobalShortcutListener +{ + /** + * Keybindings service. + */ + private KeybindingsService keybindingsService = + GlobalShortcutActivator.getKeybindingsService(); + + /** + * Callback when an shortcut is typed + * + * @param evt GlobalShortcutEvent + */ + public void shortcutReceived(GlobalShortcutEvent evt) + { + AWTKeyStroke keystroke = evt.getKeyStroke(); + GlobalKeybindingSet set = keybindingsService.getGlobalBindings(); + + for(Map.Entry> entry : + set.getBindings().entrySet()) + { + for(AWTKeyStroke ks : entry.getValue()) + { + if(entry.getKey().equals("contactlist") && + keystroke.getKeyCode() == ks.getKeyCode() && + keystroke.getModifiers() == ks.getModifiers()) + { + ExportedWindow window = + GlobalShortcutActivator.getUIService(). + getExportedWindow(ExportedWindow.MAIN_WINDOW); + + if(window == null) + return; + + window.bringToFront(); + window.setVisible(true); + if(window instanceof Window) + { + ((Window)window).setAlwaysOnTop(true); + ((Window)window).setAlwaysOnTop(false); + } + } + } + } + } +} diff --git a/src/net/java/sip/communicator/impl/globalshortcut/globalshortcut.manifest.mf b/src/net/java/sip/communicator/impl/globalshortcut/globalshortcut.manifest.mf new file mode 100644 index 000000000..97cbd1b53 --- /dev/null +++ b/src/net/java/sip/communicator/impl/globalshortcut/globalshortcut.manifest.mf @@ -0,0 +1,13 @@ +Bundle-Activator: net.java.sip.communicator.impl.globalshortcut.GlobalShortcutActivator +Bundle-Name: Global shortcut +Bundle-Description: A bundle which implements global shortcut +Bundle-Vendor: sip-communicator.org +Bundle-Version: 0.0.1 +System-Bundle: yes +Import-Package: org.osgi.framework, + net.java.sip.communicator.service.protocol, + net.java.sip.communicator.service.protocol.event, + net.java.sip.communicator.service.keybindings, + net.java.sip.communicator.service.gui, + net.java.sip.communicator.util, +Export-package: net.java.sip.communicator.service.globalshortcut, \ No newline at end of file diff --git a/src/net/java/sip/communicator/impl/gui/GuiActivator.java b/src/net/java/sip/communicator/impl/gui/GuiActivator.java index a4b1ea30d..ff324785d 100644 --- a/src/net/java/sip/communicator/impl/gui/GuiActivator.java +++ b/src/net/java/sip/communicator/impl/gui/GuiActivator.java @@ -48,6 +48,9 @@ public class GuiActivator implements BundleActivator private static UIServiceImpl uiService = null; + /** + * OSGi bundle context. + */ public static BundleContext bundleContext; private static ConfigurationService configService; @@ -172,7 +175,7 @@ public void stop(BundleContext bContext) throws Exception /** * Returns all ProtocolProviderFactorys obtained from the bundle * context. - * + * * @return all ProtocolProviderFactorys obtained from the bundle * context */ @@ -193,9 +196,9 @@ public void stop(BundleContext bContext) throws Exception logger.error("LoginManager : " + e); } - if (serRefs != null) + if (serRefs != null) { - for (ServiceReference serRef : serRefs) + for (ServiceReference serRef : serRefs) { ProtocolProviderFactory providerFactory = (ProtocolProviderFactory) @@ -332,7 +335,7 @@ public static List getRegisteredProviders( * given operationSetClass. * * @param opSetClass the operation set class for which we're looking - * for providers + * for providers * @return a list of all currently registered providers, which support the * given operationSetClass */ @@ -672,7 +675,7 @@ public static List getContactSources() logger.error("GuiActivator : " + e); } - if (serRefs != null) + if (serRefs != null) { for (ServiceReference serRef : serRefs) { @@ -688,7 +691,7 @@ public static List getContactSources() /** * Returns all ReplacementServices obtained from the bundle * context. - * + * * @return all ReplacementService implementation obtained from the * bundle context */ @@ -726,7 +729,7 @@ public static Map getReplacementSources() /** * Returns the SmiliesReplacementService obtained from the bundle * context. - * + * * @return the SmiliesReplacementService implementation obtained * from the bundle context */ @@ -744,7 +747,7 @@ public static SmiliesReplacementService getSmiliesReplacementSource() /** * Returns the PhoneNumberI18nService obtained from the bundle * context. - * + * * @return the PhoneNumberI18nService implementation obtained * from the bundle context */ @@ -781,6 +784,7 @@ public static SecurityAuthority getSecurityAuthority() * Returns the SecurityAuthority implementation registered to * handle security authority events. * + * @param protocolName protocol name * @return the SecurityAuthority implementation obtained * from the bundle context */ @@ -896,7 +900,7 @@ public static ProtocolProviderService getPreferredAccount() // is it the preferred protocol ? if(wizard.getClass().getName().equals(prefWName)) { - ArrayList registeredAccounts + ArrayList registeredAccounts = getProtocolProviderFactory(wizard.getProtocolName()) .getRegisteredAccounts(); diff --git a/src/net/java/sip/communicator/impl/gui/UIServiceImpl.java b/src/net/java/sip/communicator/impl/gui/UIServiceImpl.java index 6beb3f7ee..f124ec6e4 100644 --- a/src/net/java/sip/communicator/impl/gui/UIServiceImpl.java +++ b/src/net/java/sip/communicator/impl/gui/UIServiceImpl.java @@ -380,7 +380,7 @@ public void move(int x, int y) */ public void bringToFront() { - if (mainFrame.getState() == Frame.ICONIFIED) + if (mainFrame.getState() == Frame.ICONIFIED) mainFrame.setState(Frame.NORMAL); // Because toFront() method gives us no guarantee that our frame would // go on top we'll try to also first request the focus and set our diff --git a/src/net/java/sip/communicator/impl/gui/main/call/CallManager.java b/src/net/java/sip/communicator/impl/gui/main/call/CallManager.java index 5b9d0c1d9..e9df33a7d 100644 --- a/src/net/java/sip/communicator/impl/gui/main/call/CallManager.java +++ b/src/net/java/sip/communicator/impl/gui/main/call/CallManager.java @@ -92,6 +92,18 @@ public void callStateChanged(CallChangeEvent evt) if (receivedCallDialog.isVisible()) receivedCallDialog.setVisible(false); + // Ensure that the CallDialog is created, because for now + // it is the one that listens for CallPeers. + Call call = evt.getSourceCall(); + if ((evt.getNewValue() + .equals(CallState.CALL_INITIALIZATION) + || evt.getNewValue() + .equals(CallState.CALL_IN_PROGRESS)) + && activeCalls.get(call) == null) + { + openCallContainer(call); + } + if (evt.getNewValue().equals(CallState.CALL_ENDED)) { if (evt.getOldValue() @@ -622,6 +634,8 @@ public static void createCall( String callString, * @param callString the string to call * @param c the component, which indicates where should be shown the "call * via" menu if needed + * @param l listener that is notified when the call interface has been + * started after call was created */ public static void createCall( String callString, JComponent c, @@ -1494,7 +1508,7 @@ private static class EnableLocalVideoThread /** * Creates the enable local video call thread. * - * @param call the call, for which to enable/disable + * @param call the call, for which to enable/disable * @param enable */ public EnableLocalVideoThread(Call call, boolean enable) diff --git a/src/net/java/sip/communicator/impl/gui/main/call/PreCallDialog.java b/src/net/java/sip/communicator/impl/gui/main/call/PreCallDialog.java index be411bfe8..b425dbe01 100644 --- a/src/net/java/sip/communicator/impl/gui/main/call/PreCallDialog.java +++ b/src/net/java/sip/communicator/impl/gui/main/call/PreCallDialog.java @@ -18,7 +18,7 @@ import com.explodingpixels.macwidgets.*; /** - * + * * @author Yana Stamcheva */ public abstract class PreCallDialog @@ -160,7 +160,7 @@ private Window createPreCallWindow( String title, receivedCallWindow.setAlwaysOnTop(true); // prevents dialog window to get unwanted key events and when going - // on top on linux, it steals focus and if we are accedently + // on top on linux, it steals focus and if we are accidently // writing something and pressing enter a call get answered receivedCallWindow.setFocusableWindowState(false); diff --git a/src/net/java/sip/communicator/impl/gui/main/contactlist/ContactList.java b/src/net/java/sip/communicator/impl/gui/main/contactlist/ContactList.java index a69e3a69b..ae2a39224 100644 --- a/src/net/java/sip/communicator/impl/gui/main/contactlist/ContactList.java +++ b/src/net/java/sip/communicator/impl/gui/main/contactlist/ContactList.java @@ -37,6 +37,11 @@ public class ContactList implements MetaContactListListener, MouseListener { + /** + * Serial version UID. + */ + private static final long serialVersionUID = 0L; + private static final String ADD_OPERATION = "AddOperation"; private static final String REMOVE_OPERATION = "RemoveOperation"; @@ -392,7 +397,7 @@ public void fireContactListEvent(Object source, int eventID, int clickCount) * * @param sourceContact the contact that this event is about * @param eventID the id indicating the exact type of the event to fire. - * @param clickCount + * @param clickCount */ public void fireContactListEvent( MetaContact sourceContact, int eventID, @@ -404,7 +409,7 @@ public void fireContactListEvent( MetaContact sourceContact, } /** - * + * * @param contactListListeners * @param event */ @@ -1273,7 +1278,7 @@ public void run() } catch (MetaContactListException e) { - + } return; diff --git a/src/net/java/sip/communicator/impl/gui/main/contactlist/ContactListPane.java b/src/net/java/sip/communicator/impl/gui/main/contactlist/ContactListPane.java index d298afe67..e64e0e5b5 100644 --- a/src/net/java/sip/communicator/impl/gui/main/contactlist/ContactListPane.java +++ b/src/net/java/sip/communicator/impl/gui/main/contactlist/ContactListPane.java @@ -18,7 +18,6 @@ import net.java.sip.communicator.impl.gui.event.*; import net.java.sip.communicator.impl.gui.main.*; import net.java.sip.communicator.impl.gui.main.chat.*; -import net.java.sip.communicator.impl.gui.utils.*; import net.java.sip.communicator.service.contacteventhandler.*; import net.java.sip.communicator.service.contactlist.*; import net.java.sip.communicator.service.gui.*; @@ -45,6 +44,11 @@ public class ContactListPane ContactListListener, PluginComponentListener { + /** + * Serial version UID. + */ + private static final long serialVersionUID = 0L; + private final MainFrame mainFrame; private TreeContactList contactList; @@ -549,6 +553,11 @@ public CommonRightButtonMenu getCommonRightButtonMenu() */ private class TypingTimer extends Timer { + /** + * Serial version UID. + */ + private static final long serialVersionUID = 0L; + private MetaContact metaContact; public TypingTimer() diff --git a/src/net/java/sip/communicator/impl/keybindings/GlobalKeybindingSetImpl.java b/src/net/java/sip/communicator/impl/keybindings/GlobalKeybindingSetImpl.java new file mode 100644 index 000000000..0b91cfdba --- /dev/null +++ b/src/net/java/sip/communicator/impl/keybindings/GlobalKeybindingSetImpl.java @@ -0,0 +1,53 @@ +/* + * Jitsi, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. + * See terms of license at gnu.org. + */ +package net.java.sip.communicator.impl.keybindings; + +import java.awt.*; +import java.util.*; +import java.util.List; + +import net.java.sip.communicator.service.keybindings.*; + +/** + * Global keybinding set. + * + * @author Sebastien Vincent + */ +public class GlobalKeybindingSetImpl + implements GlobalKeybindingSet +{ + /** + * List of bindings (name and list of different keystroke that would + * trigger the action). + */ + private Map> bindings = new + LinkedHashMap>(); + + /** + * Provides current keybinding mappings. + * @return mapping of keystrokes to the string representation of the actions + * they perform + */ + public Map> getBindings() + { + return new LinkedHashMap>(this.bindings); + } + + /** + * Resets the bindings and notifies the observer's listeners if they've + * changed. + * @param bindings new keybindings to be held + */ + public void setBindings(Map> bindings) + { + if (!this.bindings.equals(bindings)) + { + this.bindings.clear(); + this.bindings.putAll(bindings); + } + } +} diff --git a/src/net/java/sip/communicator/impl/keybindings/KeybindingsActivator.java b/src/net/java/sip/communicator/impl/keybindings/KeybindingsActivator.java index 526148e5c..85bc366b2 100644 --- a/src/net/java/sip/communicator/impl/keybindings/KeybindingsActivator.java +++ b/src/net/java/sip/communicator/impl/keybindings/KeybindingsActivator.java @@ -6,6 +6,7 @@ */ package net.java.sip.communicator.impl.keybindings; +import net.java.sip.communicator.service.configuration.*; import net.java.sip.communicator.service.keybindings.*; import net.java.sip.communicator.util.*; @@ -32,6 +33,16 @@ public class KeybindingsActivator */ private KeybindingsServiceImpl keybindingsService = null; + /** + * Reference to the configuration service + */ + private static ConfigurationService configService; + + /** + * OSGi bundle context. + */ + private static BundleContext bundleContext = null; + /** * Called when this bundle is started. * @@ -41,6 +52,8 @@ public void start(BundleContext context) { if (this.keybindingsService == null) { + bundleContext = context; + if (logger.isDebugEnabled()) logger.debug("Service Impl: " + getClass().getName() + " [ STARTED ]"); @@ -65,4 +78,25 @@ public void stop(BundleContext context) this.keybindingsService = null; } } + + /** + * Returns a reference to a ConfigurationService implementation currently + * registered in the bundle context or null if no such implementation was + * found. + * + * @return a currently valid implementation of the ConfigurationService. + */ + public static ConfigurationService getConfigService() + { + if(configService == null) + { + ServiceReference confReference + = bundleContext.getServiceReference( + ConfigurationService.class.getName()); + configService + = (ConfigurationService) bundleContext.getService( + confReference); + } + return configService; + } } diff --git a/src/net/java/sip/communicator/impl/keybindings/KeybindingsServiceImpl.java b/src/net/java/sip/communicator/impl/keybindings/KeybindingsServiceImpl.java index 10f2fbdd5..86d52a505 100644 --- a/src/net/java/sip/communicator/impl/keybindings/KeybindingsServiceImpl.java +++ b/src/net/java/sip/communicator/impl/keybindings/KeybindingsServiceImpl.java @@ -6,14 +6,17 @@ */ package net.java.sip.communicator.impl.keybindings; +import java.awt.*; import java.io.*; import java.text.*; import java.util.*; +import java.util.List; //disambiguation import javax.swing.*; import org.osgi.framework.*; +import net.java.sip.communicator.service.configuration.*; import net.java.sip.communicator.service.fileaccess.*; import net.java.sip.communicator.service.keybindings.*; import net.java.sip.communicator.util.*; @@ -54,6 +57,12 @@ class KeybindingsServiceImpl */ private static final String CUSTOM_KEYBINDING_DIR = "keybindings"; + /** + * Path where to store the global shortcuts. + */ + private final static String CONFIGURATION_PATH = + "net.java.sip.communicator.impl.keybinding.global"; + /** * Flag indicating if service is running. */ @@ -65,6 +74,11 @@ class KeybindingsServiceImpl private final HashMap bindings = new HashMap(); + /** + * Loaded global keybinding mappings. + */ + private GlobalKeybindingSet globalBindings = null; + /** * Starts the KeybindingService, for each keybinding category retrieving the * default bindings then overwriting them with any custom bindings that can @@ -216,6 +230,9 @@ synchronized void start(BundleContext bc) new KeybindingSetImpl(merged, category, customFile); this.bindings.put(category, newSet); newSet.addObserver(this); + + globalBindings = new GlobalKeybindingSetImpl(); + globalBindings.setBindings(getGlobalShortcutFromConfiguration()); } this.isRunning = true; @@ -295,4 +312,130 @@ public void update(Observable obs, Object arg) } } } + + /** + * Returns list of global shortcuts from the configuration file. + * + * @return list of global shortcuts. + */ + public Map> getGlobalShortcutFromConfiguration() + { + Map> gBindings = new + LinkedHashMap>(); + ConfigurationService configService = + KeybindingsActivator.getConfigService(); + List kss = new ArrayList(); + String name = null; + String shortcut = null; + String shortcut2 = null; + String propName = null; + String propName2 = null; + + name = "answer"; + propName = CONFIGURATION_PATH + ".answer.1"; + propName2 = CONFIGURATION_PATH + ".answer.2"; + + shortcut = propName != null ? + (String)configService.getProperty(propName) : null; + shortcut2 = propName2 != null ? + (String)configService.getProperty(propName2) : null; + if(shortcut != null) + { + kss.add(AWTKeyStroke.getAWTKeyStroke(shortcut)); + } + if(shortcut2 != null) + { + kss.add(AWTKeyStroke.getAWTKeyStroke(shortcut2)); + } + gBindings.put(name, kss); + + name = "hangup"; + propName = CONFIGURATION_PATH + ".hangup.1"; + propName2 = CONFIGURATION_PATH + ".hangup.2"; + shortcut = propName != null ? + (String)configService.getProperty(propName) : null; + shortcut2 = propName2 != null ? + (String)configService.getProperty(propName2) : null; + kss = new ArrayList(); + + if(shortcut != null) + { + kss.add(AWTKeyStroke.getAWTKeyStroke(shortcut)); + } + if(shortcut2 != null) + { + kss.add(AWTKeyStroke.getAWTKeyStroke(shortcut2)); + } + gBindings.put(name, kss); + + name = "contactlist"; + propName = CONFIGURATION_PATH + ".contactlist.1"; + propName2 = CONFIGURATION_PATH + ".contactlist.2"; + shortcut = propName != null ? + (String)configService.getProperty(propName) : null; + shortcut2 = propName2 != null ? + (String)configService.getProperty(propName2) : null; + kss = new ArrayList(); + + if(shortcut != null) + { + kss.add(AWTKeyStroke.getAWTKeyStroke(shortcut)); + } + if(shortcut2 != null) + { + kss.add(AWTKeyStroke.getAWTKeyStroke(shortcut2)); + } + gBindings.put(name, kss); + + return gBindings; + } + + /** + * Save the configuration file. + */ + public void saveGlobalShortcutFromConfiguration() + { + ConfigurationService configService = + KeybindingsActivator.getConfigService(); + String shortcut = null; + String shortcut2 = null; + + for(Map.Entry> entry : + globalBindings.getBindings().entrySet()) + { + String key = entry.getKey(); + List kss = entry.getValue(); + String path = CONFIGURATION_PATH; + + if(key.equals("answer")) + { + path += ".answer"; + } + else if(key.equals("hangup")) + { + path += ".hangup"; + } + else if(key.equals("contactlist")) + { + path += ".contactlist"; + } + + shortcut = path + ".1"; + shortcut2 = path + ".2"; + configService.setProperty(shortcut, kss.size() > 0 ? + kss.get(0) : null); + configService.setProperty(shortcut2, kss.size() > 1 ? + kss.get(1) : null); + } + } + + /** + * Provides the bindings associated with the global category. + * + * @return global keybinding set + */ + public GlobalKeybindingSet getGlobalBindings() + { + return globalBindings; + } } diff --git a/src/net/java/sip/communicator/impl/keybindings/keybindings.manifest.mf b/src/net/java/sip/communicator/impl/keybindings/keybindings.manifest.mf index dfacbec69..a9634150b 100644 --- a/src/net/java/sip/communicator/impl/keybindings/keybindings.manifest.mf +++ b/src/net/java/sip/communicator/impl/keybindings/keybindings.manifest.mf @@ -8,5 +8,6 @@ Import-Package: org.osgi.framework, net.java.sip.communicator.util, net.java.sip.communicator.service.fileaccess, net.java.sip.communicator.service.gui, + net.java.sip.communicator.service.configuration, javax.swing Export-Package: net.java.sip.communicator.service.keybindings diff --git a/src/net/java/sip/communicator/plugin/keybindingchooser/KeybindingChooserActivator.java b/src/net/java/sip/communicator/plugin/keybindingchooser/KeybindingChooserActivator.java index 39dc4dfee..29e9d1278 100644 --- a/src/net/java/sip/communicator/plugin/keybindingchooser/KeybindingChooserActivator.java +++ b/src/net/java/sip/communicator/plugin/keybindingchooser/KeybindingChooserActivator.java @@ -8,7 +8,10 @@ import java.util.*; +import net.java.sip.communicator.service.configuration.*; +import net.java.sip.communicator.service.globalshortcut.*; import net.java.sip.communicator.service.gui.*; +import net.java.sip.communicator.service.keybindings.*; import net.java.sip.communicator.service.resources.*; import net.java.sip.communicator.util.*; @@ -16,7 +19,7 @@ /** * Enabling and disabling osgi functionality for the keybinding chooser. - * + * * @author Damian Johnson */ public class KeybindingChooserActivator @@ -38,6 +41,21 @@ public class KeybindingChooserActivator */ public static ResourceManagementService resourcesService; + /** + * Reference to the configuration service + */ + private static ConfigurationService configService; + + /** + * Reference to the keybinding service + */ + private static KeybindingsService keybindingService = null; + + /** + * Reference to the global shortcut service + */ + private static GlobalShortcutService globalShortcutService = null; + /** * Starts this bundle and adds the * KeybindingsConfigPanel contained in it to the configuration @@ -93,4 +111,67 @@ public static ResourceManagementService getResources() ResourceManagementServiceUtils.getService(bundleContext); return resourcesService; } + + /** + * Returns a reference to a ConfigurationService implementation currently + * registered in the bundle context or null if no such implementation was + * found. + * + * @return a currently valid implementation of the ConfigurationService. + */ + public static ConfigurationService getConfigService() + { + if(configService == null) + { + ServiceReference confReference + = bundleContext.getServiceReference( + ConfigurationService.class.getName()); + configService + = (ConfigurationService) bundleContext.getService( + confReference); + } + return configService; + } + + /** + * Returns a reference to a KeybindingsService implementation currently + * registered in the bundle context or null if no such implementation was + * found. + * + * @return a currently valid implementation of the KeybindingsService. + */ + public static KeybindingsService getKeybindingsService() + { + if(keybindingService == null) + { + ServiceReference keybindingReference + = bundleContext.getServiceReference( + KeybindingsService.class.getName()); + keybindingService + = (KeybindingsService) bundleContext.getService( + keybindingReference); + } + return keybindingService; + } + + /** + * Returns a reference to a GlobalShortcutService implementation currently + * registered in the bundle context or null if no such implementation was + * found. + * + * @return a currently valid implementation of the GlobalShortcutService. + */ + public static GlobalShortcutService getGlobalShortcutService() + { + if(globalShortcutService == null) + { + ServiceReference globalShortcutReference + = bundleContext.getServiceReference( + GlobalShortcutService.class.getName()); + globalShortcutService + = (GlobalShortcutService) bundleContext.getService( + globalShortcutReference); + } + return globalShortcutService; + } } diff --git a/src/net/java/sip/communicator/plugin/keybindingchooser/KeybindingsConfigPanel.java b/src/net/java/sip/communicator/plugin/keybindingchooser/KeybindingsConfigPanel.java index 1926b3218..40a2a74d8 100644 --- a/src/net/java/sip/communicator/plugin/keybindingchooser/KeybindingsConfigPanel.java +++ b/src/net/java/sip/communicator/plugin/keybindingchooser/KeybindingsConfigPanel.java @@ -15,13 +15,14 @@ import org.osgi.framework.*; import net.java.sip.communicator.plugin.keybindingchooser.chooser.*; +import net.java.sip.communicator.plugin.keybindingchooser.globalchooser.*; import net.java.sip.communicator.service.keybindings.*; import net.java.sip.communicator.util.swing.*; /** * The ConfigurationForm that would be added to the settings * configuration to configure the application keybindings. - * + * * @author Damian Johnson * @author Lubomir Marinov */ @@ -44,6 +45,9 @@ private static KeybindingsService getKeybindingsService() private final HashMap choosers = new HashMap(); + /** + * Constructor. + */ public KeybindingsConfigPanel() { super(new BorderLayout()); @@ -74,7 +78,7 @@ public void focusLost(FocusEvent event) continue; // defaults failed to load SIPChooser newChooser = new SIPChooser(); - newChooser.putAllBindings(bindingSet.getBindings()); + newChooser.putAllBindings(bindingSet); JPanel chooserWrapper = new TransparentPanel(new BorderLayout()); chooserWrapper.add(newChooser, BorderLayout.NORTH); @@ -88,25 +92,12 @@ public void focusLost(FocusEvent event) this.choosers.put(bindingSet, newChooser); } - add(chooserPanes); - - JButton apply = new JButton( - KeybindingChooserActivator.getResources() - .getI18NString("service.gui.APPLY")); + // global shortcut + GlobalShortcutConfigForm globalBindingPanel = + new GlobalShortcutConfigForm(); + chooserPanes.addTab("Global shortcut", globalBindingPanel); - apply.addActionListener(new ActionListener() - { - public void actionPerformed(ActionEvent event) - { - for (KeybindingSet set : choosers.keySet()) - set.setBindings(choosers.get(set).getBindingMap()); - } - }); - - JPanel bottomWrapper = - new TransparentPanel(new FlowLayout(FlowLayout.RIGHT)); - bottomWrapper.add(apply); - add(bottomWrapper, BorderLayout.SOUTH); + add(chooserPanes); } /** @@ -114,7 +105,7 @@ public void actionPerformed(ActionEvent event) * underscores and this changes the input to lowercase except the first * letter of each word. For instance, "RARE_CARDS" would become "Rare * Cards". - * + * * @param input string to be converted * @return reader friendly variant of constant name */ @@ -188,7 +179,7 @@ public boolean putBinding(BindingEntry newEntry, int index) * given in its plugin-specific format. The key is translated to the * global format of the ReouseceManagementService and the translated key * is used to retrieve the string from the resource files. - * + * * @param key the key of the string to be retrieved given in its * plugin-specific format * @return the internationalized string corresponding to a specific key diff --git a/src/net/java/sip/communicator/plugin/keybindingchooser/chooser/BindingAdaptor.java b/src/net/java/sip/communicator/plugin/keybindingchooser/chooser/BindingAdaptor.java index fc5d559d6..31f0fb009 100644 --- a/src/net/java/sip/communicator/plugin/keybindingchooser/chooser/BindingAdaptor.java +++ b/src/net/java/sip/communicator/plugin/keybindingchooser/chooser/BindingAdaptor.java @@ -1,3 +1,9 @@ +/* + * Jitsi, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. + * See terms of license at gnu.org. + */ package net.java.sip.communicator.plugin.keybindingchooser.chooser; import java.awt.event.*; @@ -9,7 +15,7 @@ * BindingChooser. This can be added to focused components to provide editing * functionality for this chooser. This prevents duplicate entries and varies * how it captures input according to the type of key event its set to capture. - * + * * @author Damian Johnson (atagar1@gmail.com) * @version August 19, 2007 */ @@ -36,7 +42,7 @@ public class BindingAdaptor /** * Provides if bindings are currently disableable via generated key adaptors * or not. - * + * * @return true if input can disable bindings, false otherwise. */ public boolean isBindingDisablingEnabled() @@ -47,7 +53,7 @@ public boolean isBindingDisablingEnabled() /** * Sets if bindings can be disabled via generated key adaptors with the * disabling key code. By default this is false. - * + * * @param enable if true then input can disable bindings, otherwise bindings * may not be disabled */ @@ -59,7 +65,7 @@ public void setBindingsDisableable(boolean enable) /** * Provides the keycode that can be input to generated key adaptors to * disable key bindings. - * + * * @return keycode of disabling input */ public int getDisablingKeyCode() @@ -72,7 +78,7 @@ public int getDisablingKeyCode() * from returned mappings) via generated key adaptors. This only works if * the set event type is KEY_PRESSED or KEY_RELEASED since KEY_TYPED events * fail to provide keycodes. By default this is VK_ESCAPE. - * + * * @param keycode keycode that sets selected entry to a disabled state */ public void setDisablingKeyCode(int keycode) @@ -83,7 +89,7 @@ public void setDisablingKeyCode(int keycode) /** * Provides the type of keystroke registered by input via generated key * adaptors. - * + * * @return type of input detected by generated key adaptors */ public int getInputEventType() @@ -95,7 +101,7 @@ public int getInputEventType() * Sets the type of keystroke registered by input via generated key adaptors * (by default KeyEvent.KEY_PRESSED). This must be a valid type of key event * which includes: KEY_PRESSED, KEY_RELEASED, or KEY_TYPED. - * + * * @param type type of keystroke registered by input * @throws IllegalArgumentException if type doesn't match a valid key event */ diff --git a/src/net/java/sip/communicator/plugin/keybindingchooser/chooser/BindingChooser.java b/src/net/java/sip/communicator/plugin/keybindingchooser/chooser/BindingChooser.java index 7ab7ab779..96f52438c 100644 --- a/src/net/java/sip/communicator/plugin/keybindingchooser/chooser/BindingChooser.java +++ b/src/net/java/sip/communicator/plugin/keybindingchooser/chooser/BindingChooser.java @@ -1,3 +1,9 @@ +/* + * Jitsi, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. + * See terms of license at gnu.org. + */ package net.java.sip.communicator.plugin.keybindingchooser.chooser; import java.awt.*; @@ -6,6 +12,7 @@ import javax.swing.*; +import net.java.sip.communicator.service.keybindings.*; import net.java.sip.communicator.util.swing.*; /** @@ -17,7 +24,7 @@ * particularly recommended unless automated changes to the appearance (the * indentation style and color scheme) are disabled since they may be * unexpectedly reverted or clash any alterations made. - * + * * @author Damian Johnson (atagar1@gmail.com) * @version September 1, 2007 */ @@ -35,13 +42,18 @@ public class BindingChooser private String selectedText = "Press shortcut..."; + /** + * Keybinding set. + */ + private KeybindingSet set = null; + /** * Displays a dialog allowing the user to redefine the keystroke component * of key bindings. The top has light blue labels describing the fields and * the bottom provides an 'OK' and 'Cancel' option. This uses the default * color scheme and indent style. If no entries are selected then the enter * key is equivalent to pressing 'OK' and escape is the same as 'Cancel'. - * + * * @param parent frame to which to apply modal property and center within * (centers within screen if null) * @param bindings initial mapping of keystrokes to their actions @@ -57,6 +69,20 @@ public static LinkedHashMap showDialog(Component parent, .makeAdaptor()); } + /** + * Adds a collection of new key binding mappings to the end of the listing. + * If any shortcuts are already contained then the previous entries are + * replaced (not triggering the onUpdate method). Disabled shortcuts trigger + * replacement on duplicate actions instead. + * + * @param set mapping between keystrokes and actions to be added + */ + public void putAllBindings(KeybindingSet set) + { + this.set = set; + putAllBindings(set.getBindings()); + } + /** * Displays a dialog allowing the user to redefine the keystroke component * of key bindings. The bottom provides an 'OK' and 'Cancel' option. If no @@ -67,7 +93,7 @@ public static LinkedHashMap showDialog(Component parent, * setting the selected shortcut field. Also note that labels use the * default entry size and should be omitted if using content with custom * dimensions. - * + * * @param parent frame to which to apply modal property and center within * (centers within screen if null) * @param display body of the display, containing current bindings and @@ -185,7 +211,7 @@ protected void onClick(MouseEvent event, BindingEntry entry, /** * Sets if the shortcut fields of entries can be selected to provide editing * functionality or not. If false, any selected entry is deselected. - * + * * @param editable if true shortcut fields may be selected to have their * values changed, otherwise user input and calls to the * setSelected method are ignored @@ -201,7 +227,7 @@ public void setEditable(boolean editable) /** * Provides the indent style used by the chooser. - * + * * @return type of content in the indent field */ public IndentStyle getIndentStyle() @@ -212,7 +238,7 @@ public IndentStyle getIndentStyle() /** * Sets content display in the indent field of entries. This will prompt an * onUpdate on all entries unless setting the style to NONE. - * + * * @param style type of content displayed in entry's indent field */ public void setIndentStyle(IndentStyle style) @@ -231,7 +257,7 @@ public void setIndentStyle(IndentStyle style) /** * Sets the message of the selected shortcut field when awaiting user input. * By default this is "Press shortcut...". - * + * * @param message prompt for user input */ public void setSelectedText(String message) @@ -246,7 +272,7 @@ public void setSelectedText(String message) /** * Returns if a binding is currently awaiting input or not. - * + * * @return true if a binding is awaiting input, false otherwise */ public boolean isBindingSelected() @@ -256,7 +282,7 @@ public boolean isBindingSelected() /** * Provides the currently selected entry if awaiting input. - * + * * @return entry currently awaiting input, if one exists */ public BindingEntry getSelected() @@ -270,7 +296,7 @@ public BindingEntry getSelected() * currently selected entry is deselected. If null, then this simply reverts * any selections (leaving no entry selected). The onUpdate method is called * whenever an entry is either selected or deselected. - * + * * @param entry binding entry awaiting input for its shortcut field * @throws IllegalArgumentException if entry is not contained in chooser */ @@ -311,7 +337,7 @@ public void setSelected(BindingEntry entry) /** * Provides a key adaptor that can provide editing functionality for the * selected entry. - * + * * @return binding adaptor configured to this chooser */ public BindingAdaptor makeAdaptor() @@ -363,7 +389,7 @@ else if (field == BindingEntry.Field.SHORTCUT) /** * Emulates keyboard input, setting the selected entry's shortcut if an * entry's currently awaiting input. - * + * * @param input keystroke input for selected entry */ void doInput(KeyStroke input) @@ -371,6 +397,8 @@ void doInput(KeyStroke input) if (isBindingSelected()) { this.selectedEntry.setShortcut(input); + //apply configuration + set.setBindings(this.getBindingMap()); // TYPE indent can change according to the shortcut // this.indentStyle.apply(this.selectedEntry, @@ -408,7 +436,7 @@ public static enum IndentStyle /** * Returns the enum representation of a string. This is case sensitive. - * + * * @param str toString representation of this enum * @return enum associated with a string * @throws IllegalArgumentException if argument is not represented by @@ -480,7 +508,7 @@ public String toString() * underscores and this changes the input to lowercase except the first * letter of each word. For instance, "RARE_CARDS" would become * "Rare Cards". - * + * * @param input string to be converted * @return reader friendly variant of constant name */ diff --git a/src/net/java/sip/communicator/plugin/keybindingchooser/chooser/BindingEntry.java b/src/net/java/sip/communicator/plugin/keybindingchooser/chooser/BindingEntry.java index 0db0998c8..00abfabe8 100644 --- a/src/net/java/sip/communicator/plugin/keybindingchooser/chooser/BindingEntry.java +++ b/src/net/java/sip/communicator/plugin/keybindingchooser/chooser/BindingEntry.java @@ -1,3 +1,9 @@ +/* + * Jitsi, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. + * See terms of license at gnu.org. + */ package net.java.sip.communicator.plugin.keybindingchooser.chooser; import java.awt.*; @@ -9,7 +15,7 @@ /** * Display element for a single key binding. - * + * * @author Damian Johnson (atagar1@gmail.com) * @version August 7, 2007 */ @@ -130,7 +136,7 @@ public boolean isDisabled() /** * Provides the label associated with a field. - * + * * @param field element of display to be returned * @return label associated with field */ @@ -165,7 +171,7 @@ public enum Field * the representation are unspecified and subject to change but the * following format can be considered to be typical:
* "BindingEntry (" + Shortcut + " \u2192 " + Action + ")" - * + * * @return string representation of entry */ @Override @@ -188,7 +194,7 @@ public String toString() * Checks if argument is an instance of this class with the same shortcut * and associated action. It does not compare aspects of the display * elements. - * + * * @param obj element with which to be compared * @return true if argument is an instance of this class with matching * shortcut and action, false otherwise diff --git a/src/net/java/sip/communicator/plugin/keybindingchooser/chooser/BindingPanel.java b/src/net/java/sip/communicator/plugin/keybindingchooser/chooser/BindingPanel.java index f84bca93c..ab9e7ca0f 100644 --- a/src/net/java/sip/communicator/plugin/keybindingchooser/chooser/BindingPanel.java +++ b/src/net/java/sip/communicator/plugin/keybindingchooser/chooser/BindingPanel.java @@ -1,3 +1,9 @@ +/* + * Jitsi, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. + * See terms of license at gnu.org. + */ package net.java.sip.communicator.plugin.keybindingchooser.chooser; import java.awt.*; @@ -22,7 +28,7 @@ * shortcuts aren't supported. An exception is made in the case of disabled * shortcuts, but to keep mappings unique duplicate actions among disabled * entries are not permitted. - * + * * @author Damian Johnson (atagar1@gmail.com) * @version September 1, 2007 */ @@ -35,7 +41,7 @@ public abstract class BindingPanel * Method called whenever an entry is either added or shifts in the display. * For instance, if the second entry is removed then this is called on the * third to last elements. - * + * * @param index newly assigned index of entry * @param entry entry that has been added or shifted * @param isNew if true the entry is new to the display, false otherwise @@ -45,7 +51,7 @@ protected abstract void onUpdate(int index, BindingEntry entry, /** * Method called upon any mouse clicks within a BindingEntry in the display. - * + * * @param event fired mouse event that triggered method call * @param entry entry on which the click landed * @param field field of entry on which the click landed, null if not a @@ -66,13 +72,13 @@ public BindingPanel() * triggering the onUpdate method). Disabled shortcuts trigger replacement * on duplicate actions instead. This uses the normal parameters used to * generate key stokes, such as: - * + * *
      * bindingPanel.putBinding('Y', 0, "Confirm Selection");
      * bindingPanel.putBinding(KeyEvent.VK_DELETE, KeyEvent.CTRL_MASK
      *     | KeyEvent.ALT_MASK, "Kill Process");
      * 
- * + * * @param keyCode key code of keystroke component of mapping * @param modifier modifiers of keystroke component of mapping * @param action string component of mapping @@ -88,7 +94,7 @@ public boolean putBinding(int keyCode, int modifier, String action) * contains the shortcut then the previous entry is replaced instead (not * triggering the onUpdate method). Disabled shortcuts trigger replacement * on duplicate actions instead. - * + * * @param shortcut keystroke component of mapping * @param action string component of mapping * @return true if contents did not already include shortcut @@ -103,7 +109,7 @@ public boolean putBinding(KeyStroke shortcut, String action) * this already contains the shortcut then the previous entry is replaced * instead (not triggering the onUpdate method). Disabled shortcuts trigger * replacement on duplicate actions instead. - * + * * @param shortcut keystroke component of mapping * @param action string component of mapping * @param index location in which to insert mapping @@ -121,7 +127,7 @@ public boolean putBinding(KeyStroke shortcut, String action, int index) * this already contains the shortcut then the previous entry is replaced * instead (not triggering the onUpdate method). Disabled shortcuts trigger * replacement on duplicate actions instead. - * + * * @param newEntry entry to add to contents * @param index location in which to insert mapping * @return true if contents did not already include shortcut @@ -198,7 +204,7 @@ public boolean putBinding(BindingEntry newEntry, int index) * If any shortcuts are already contained then the previous entries are * replaced (not triggering the onUpdate method). Disabled shortcuts trigger * replacement on duplicate actions instead. - * + * * @param bindings mapping between keystrokes and actions to be added */ public void putAllBindings(Map bindings) @@ -211,7 +217,7 @@ public void putAllBindings(Map bindings) /** * Removes a particular binding from the contents. - * + * * @param entry binding to be removed * @return true if binding was in the contents, false otherwise */ @@ -226,7 +232,7 @@ public boolean removeBinding(BindingEntry entry) /** * Removes the binding at a particular index of the listing. - * + * * @param index from which to remove entry * @return the entry that was removed from the contents * @throws IndexOutOfBoundsException if index is out of range (index < 0 || @@ -269,7 +275,7 @@ public void clearBindings() * Returns if a keystroke is in the panel's current contents. This provides * a preemptive means of checking if adding a non-disabled shortcut would * cause a replacement. - * + * * @param shortcut keystroke to be checked against contents * @return true if contents includes the shortcut, false otherwise */ @@ -293,7 +299,7 @@ public boolean contains(KeyStroke shortcut) /** * Provides number of key bindings currently present. - * + * * @return number of key bindings in the display */ public int getBindingCount() @@ -303,7 +309,7 @@ public int getBindingCount() /** * Provides the index of a particular entry. - * + * * @param entry entry for which the index should be returned * @return entry index, -1 if not found */ @@ -314,7 +320,7 @@ public int getBindingIndex(BindingEntry entry) /** * Provides a binding at a particular index. - * + * * @param index index from which to retrieve binding. * @return the entry at the specified position in this list */ @@ -325,7 +331,7 @@ public BindingEntry getBinding(int index) /** * Provides listing of the current keybinding entries. - * + * * @return list of current entry contents */ public ArrayList getBindings() @@ -336,7 +342,7 @@ public ArrayList getBindings() /** * Provides the mapping between keystrokes and actions represented by the * contents of the display. Disabled entries aren't included in the mapping. - * + * * @return mapping between contained keystrokes and their associated actions */ public LinkedHashMap getBindingMap() @@ -356,7 +362,7 @@ public LinkedHashMap getBindingMap() /** * Provides an input map associating keystrokes to actions according to the * contents of the display. Disabled entries aren't included in the mapping. - * + * * @return input mapping between contained keystrokes and their associated * actions */ diff --git a/src/net/java/sip/communicator/plugin/keybindingchooser/globalchooser/GlobalShortcutConfigForm.java b/src/net/java/sip/communicator/plugin/keybindingchooser/globalchooser/GlobalShortcutConfigForm.java new file mode 100644 index 000000000..7d3806846 --- /dev/null +++ b/src/net/java/sip/communicator/plugin/keybindingchooser/globalchooser/GlobalShortcutConfigForm.java @@ -0,0 +1,311 @@ +/* + * Jitsi, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. + * See terms of license at gnu.org. + */ +package net.java.sip.communicator.plugin.keybindingchooser.globalchooser; + +import java.awt.*; +import java.awt.event.*; +import java.util.*; +import java.util.List; + +import javax.swing.*; +import javax.swing.event.*; + +import net.java.sip.communicator.plugin.keybindingchooser.*; +import net.java.sip.communicator.service.globalshortcut.*; +import net.java.sip.communicator.service.keybindings.*; +import net.java.sip.communicator.util.*; +import net.java.sip.communicator.util.swing.*; + +/** + * This ConfigurationForm shows the list of global shortcut + * + * @author Sebastien Vincent + */ +public class GlobalShortcutConfigForm + extends TransparentPanel + implements ListSelectionListener +{ + /** + * Serial version UID. + */ + private static final long serialVersionUID = 0L; + + /** + * The logger for this class. + */ + private static Logger logger = Logger.getLogger( + GlobalShortcutConfigForm.class); + + /** + * Displays the registered shortcuts. + */ + private JTable shortcutsTable = new JTable(); + + /** + * Contains the shortcutsTable. + */ + private JScrollPane scrollPane = new JScrollPane(); + + /** + * Contains listPanel. + */ + private JPanel mainPanel = this; + + /** + * Model for the shortcutsTable + */ + private GlobalShortcutTableModel tableModel = + new GlobalShortcutTableModel(); + + /** + * Current selected row. + */ + private int currentRow = -1; + + /** + * Current selected row. + */ + private int currentColumn = -1; + + /** + * Constructor + */ + public GlobalShortcutConfigForm() + { + super(new BorderLayout()); + logger.trace("New global shortcut configuration form."); + this.initComponents(); + } + + /** + * Initialize the swing components. + */ + private void initComponents() + { + shortcutsTable.setRowHeight(22); + shortcutsTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + shortcutsTable.setShowHorizontalLines(false); + shortcutsTable.setShowVerticalLines(false); + shortcutsTable.setModel(tableModel); + shortcutsTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); + shortcutsTable.addMouseListener(new MouseAdapter() + { + @Override + public void mouseClicked(MouseEvent e) + { + if(e.getClickCount() >= 1) + { + int row = GlobalShortcutConfigForm.this.shortcutsTable. + getSelectedRow(); + int column = GlobalShortcutConfigForm.this.shortcutsTable. + getSelectedColumn(); + if(row >= 0 && column >= 1) + { + currentRow = row; + currentColumn = column; + } + } + } + }); + + shortcutsTable.addKeyListener(new KeyAdapter() + { + private KeyEvent buffer = null; + + @Override + public void keyPressed(KeyEvent event) + { + // Reports KEY_PRESSED events on release to support modifiers + this.buffer = event; + } + + @Override + public void keyReleased(KeyEvent event) + { + if (buffer != null) + { + AWTKeyStroke input = KeyStroke.getKeyStrokeForEvent(buffer); + buffer = null; + + if(currentRow != -1) + { + GlobalShortcutEntry en = + GlobalShortcutConfigForm.this.tableModel.getEntryAt( + currentRow); + List kss = new ArrayList(); + + if(currentColumn == 1) // shortcut 1 + { + kss.add(input); + kss.add(en.getShortcut2()); + } + else if(currentColumn == 2) // shortcut 2 + { + kss.add(en.getShortcut()); + kss.add(input); + } + else + { + return; + } + + en.setShortcuts(kss); + GlobalShortcutConfigForm.this.refresh(); + GlobalShortcutConfigForm.this.saveConfig(); + } + } + } + }); + + scrollPane.getViewport().add(this.shortcutsTable); + mainPanel.add(this.scrollPane, BorderLayout.CENTER); + mainPanel.setPreferredSize(new Dimension(500, 400)); + shortcutsTable.getSelectionModel().addListSelectionListener(this); + loadConfig(); + } + + /** + * Loads configuration. + */ + private void loadConfig() + { + KeybindingsService keybindingService = + KeybindingChooserActivator.getKeybindingsService(); + + GlobalKeybindingSet set = keybindingService.getGlobalBindings(); + + for(Map.Entry> entry : + set.getBindings().entrySet()) + { + String key = entry.getKey(); + List kss = entry.getValue(); + GlobalShortcutEntry gke = null; + String desc = null; + + if(key.equals("answer")) + { + desc = Resources.getString( + "plugin.keybindings.globalchooser.ANSWER_CALL"); + } + else if(key.equals("hangup")) + { + desc = Resources.getString( + "plugin.keybindings.globalchooser.HANGUP_CALL"); + } + else if(key.equals("contactlist")) + { + desc = Resources.getString( + "plugin.keybindings.globalchooser.SHOW_CONTACTLIST"); + } + else + continue; + + gke = new GlobalShortcutEntry(desc, kss); + + tableModel.addEntry(gke); + } + refresh(); + } + + /** + * Save configuration. + */ + public void saveConfig() + { + KeybindingsService keybindingService = + KeybindingChooserActivator.getKeybindingsService(); + GlobalShortcutService globalShortcutService = + KeybindingChooserActivator.getGlobalShortcutService(); + Map> gBindings = + keybindingService.getGlobalBindings().getBindings(); + List entries = tableModel.getEntries(); + List kss = null; + + for(GlobalShortcutEntry entry : entries) + { + String desc = null; + + if(entry.getAction().equals(Resources.getString( + "plugin.keybindings.globalchooser.ANSWER_CALL"))) + { + desc = "answer"; + } + else if(entry.getAction().equals(Resources.getString( + "plugin.keybindings.globalchooser.HANGUP_CALL"))) + { + desc = "hangup"; + } + else if(entry.getAction().equals(Resources.getString( + "plugin.keybindings.globalchooser.SHOW_CONTACTLIST"))) + { + desc = "contactlist"; + + } + else + continue; + + kss = gBindings.get(desc); + kss.clear(); + kss.add(entry.getShortcut()); + kss.add(entry.getShortcut2()); + gBindings.put(desc, kss); + } + + keybindingService.saveGlobalShortcutFromConfiguration(); + globalShortcutService.reloadGlobalShortcuts(); + } + + /** + * Required by ListSelectionListener. + * + * @param e event triggered + */ + public void valueChanged(ListSelectionEvent e) + { + if(shortcutsTable.getSelectedRow() == -1) + { + currentRow = -1; + currentColumn = -1; + } + } + + /** + * refreshes the table display + */ + private void refresh() + { + tableModel.fireTableStructureChanged(); + } + + /** + * Indicates if this is an advanced configuration form. + * @return true if this is an advanced configuration form, + * otherwise it returns false + */ + public boolean isAdvanced() + { + return true; + } + + /** + * Add bindings. + * + * @param bindings list of bindings + * + public void addBindings(Map> bindings) + { + for(Map.Entry> entry : bindings.entrySet()) + { + GlobalShortcutEntry e = new GlobalShortcutEntry( + entry.getKey(), entry.getValue()); + tableModel.addEntry(e); + } + refresh(); + } + */ +} + diff --git a/src/net/java/sip/communicator/plugin/keybindingchooser/globalchooser/GlobalShortcutEntry.java b/src/net/java/sip/communicator/plugin/keybindingchooser/globalchooser/GlobalShortcutEntry.java new file mode 100644 index 000000000..b578aec40 --- /dev/null +++ b/src/net/java/sip/communicator/plugin/keybindingchooser/globalchooser/GlobalShortcutEntry.java @@ -0,0 +1,248 @@ +/* + * Jitsi, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. + * See terms of license at gnu.org. + */ +package net.java.sip.communicator.plugin.keybindingchooser.globalchooser; + +import java.util.List; //disambiguation +import java.awt.*; +import java.awt.event.*; + +/** + * Entry for a global shortcut. + * + * @author Sebastien Vincent + */ +public class GlobalShortcutEntry +{ + /** + * Serial version UID. + */ + private static final long serialVersionUID = 0L; + + /** + * Disabled keystroke. + */ + private static final AWTKeyStroke DISABLED = null; + + /** + * Action name. + */ + private String action = null; + + /** + * Primary shortcut. + */ + private AWTKeyStroke shortcut = null; + + /** + * Second shortcut. + */ + private AWTKeyStroke shortcut2 = null; + + /** + * Constructor. + * + * @param action action + * @param shortcuts list of shortcut for this action + */ + public GlobalShortcutEntry(String action, List shortcuts) + { + setAction(action); + setShortcuts(shortcuts); + } + + /** + * Returns primary shortcut if it exists. + * + * @return primary shortcut if it exists. + */ + public AWTKeyStroke getShortcut() + { + return this.shortcut; + } + + /** + * Returns second shortcut if it exists. + * + * @return second shortcut if it exists. + */ + public AWTKeyStroke getShortcut2() + { + return this.shortcut2; + } + + /** + * Set the shortcut keystroke and field. + * + * @param shortcut AWTKeyStroke + * @return string representation of the keystroke + */ + public static String getShortcutText(AWTKeyStroke shortcut) + { + if (shortcut == DISABLED) + { + return "Disabled"; + } + else + { + StringBuffer buffer = new StringBuffer(); + + if (shortcut.getKeyEventType() == KeyEvent.KEY_TYPED) + { + buffer.append(shortcut.getKeyChar()); + } + else + { + int keycode = shortcut.getKeyCode(); + int modifiers = shortcut.getModifiers(); + + // Indicates modifiers of the keystroke + boolean shiftMask = (modifiers & InputEvent.SHIFT_MASK) != 0; + boolean ctrlMask = (modifiers & InputEvent.CTRL_MASK) != 0; + boolean metaMask = (modifiers & InputEvent.META_MASK) != 0; + boolean altMask = (modifiers & InputEvent.ALT_MASK) != 0; + if (shiftMask && keycode != KeyEvent.VK_SHIFT) + buffer.append("Shift + "); + if (ctrlMask && keycode != KeyEvent.VK_CONTROL) + buffer.append("Ctrl + "); + if (metaMask && keycode != KeyEvent.VK_META) + buffer.append("Meta + "); + if (altMask && keycode != KeyEvent.VK_ALT) + buffer.append("Alt + "); + + buffer.append(KeyEvent.getKeyText(keycode)); + } + return buffer.toString(); + } + } + + /** + * Set the shortcuts for this action. + * + * @param shortcuts list of shortcuts + */ + public void setShortcuts(List shortcuts) + { + if(shortcuts.size() > 0) + { + this.shortcut = shortcuts.get(0); + } + if(shortcuts.size() > 1) + { + this.shortcut2 = shortcuts.get(1); + } + } + + /** + * Returns action string. + * + * @return action + */ + public String getAction() + { + return action; + } + + /** + * Set action string + * + * @param action action + */ + public void setAction(String action) + { + this.action = action; + } + + /** + * If this global keybindings is disabled. + * + * @return true if this global keybinding is disabled + */ + public boolean isDisabled() + { + return this.shortcut == DISABLED && this.shortcut2 == DISABLED; + } + + /** + * Provides the string representation of this mapping. The exact details of + * the representation are unspecified and subject to change but the + * following format can be considered to be typical:
+ * "BindingEntry (" + Shortcut + " \u2192 " + Action + ")" + * + * @return string representation of entry + */ + @Override + public String toString() + { + StringBuilder builder = new StringBuilder(); + builder.append("GlobalBindingEntry ("); + + if (isDisabled()) + builder.append("Disabled"); + else + builder.append(getShortcut()); + builder.append(" \u2192 "); // arrow pointing right + builder.append(getAction()); + builder.append(")"); + return builder.toString(); + } + + /** + * Checks if argument is an instance of this class with the same shortcut + * and associated action. It does not compare aspects of the display + * elements. + * + * @param obj element with which to be compared + * @return true if argument is an instance of this class with matching + * shortcut and action, false otherwise + */ + @Override + public boolean equals(Object obj) + { + if (obj == this) + return true; + else if (!(obj instanceof GlobalShortcutEntry)) + return false; + + GlobalShortcutEntry entry = (GlobalShortcutEntry) obj; + boolean equals = true; + + String action = this.getAction(); + if (action == null) + equals &= entry.getAction() == null; + else + equals &= action.equals(entry.getAction()); + + AWTKeyStroke shortcut = this.getShortcut(); + if (shortcut == null) + equals &= entry.getShortcut() == null; + else + equals &= shortcut.equals(entry.getShortcut()); + + shortcut = this.getShortcut2(); + if (shortcut == null) + equals &= entry.getShortcut() == null; + else + equals &= shortcut.equals(entry.getShortcut2()); + + return equals; + } + + /** + * Returns hashcode for this instance. + * + * @return hashcode for this instance + */ + @Override + public int hashCode() + { + int hash = 17; + hash = 37 * hash + (getAction() == null ? 0 : getAction().hashCode()); + hash = + 37 * hash + (getShortcut() == null ? 0 : getShortcut().hashCode()); + return hash; + } +} diff --git a/src/net/java/sip/communicator/plugin/keybindingchooser/globalchooser/GlobalShortcutTableModel.java b/src/net/java/sip/communicator/plugin/keybindingchooser/globalchooser/GlobalShortcutTableModel.java new file mode 100644 index 000000000..896a03a6a --- /dev/null +++ b/src/net/java/sip/communicator/plugin/keybindingchooser/globalchooser/GlobalShortcutTableModel.java @@ -0,0 +1,199 @@ +/* + * Jitsi, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. + * See terms of license at gnu.org. + */ +package net.java.sip.communicator.plugin.keybindingchooser.globalchooser; + +import java.util.*; + +import javax.swing.table.*; + +/** + * Table model for global shortcuts. + * + * @author Sebastien Vincent + */ +public class GlobalShortcutTableModel + extends AbstractTableModel +{ + /** + * Serial version UID. + */ + private static final long serialVersionUID = 0L; + + /** + * List of shortcuts. + */ + private List shortcuts = + new ArrayList(); + + /** + * Returns the title for this column + * + * @param column the column + * + * @return the title for this column + * + * @see javax.swing.table.AbstractTableModel#getColumnName + */ + public String getColumnName(int column) + { + switch(column) + { + case 0: + return Resources.getString( + "plugin.keybindings.globalchooser.SHORTCUT_NAME"); + case 1: + return Resources.getString( + "plugin.keybindings.globalchooser.SHORTCUT_PRIMARY"); + case 2: + return Resources.getString( + "plugin.keybindings.globalchooser.SHORTCUT_SECOND"); + default: + throw new IllegalArgumentException("column not found"); + } + } + + /** + * Returns the number of rows in the table + * + * @return the number of rows in the table + * @see javax.swing.table.AbstractTableModel#getRowCount + */ + public int getRowCount() + { + return shortcuts.size(); + } + + /** + * Returns the number of column in the table + * + * @return the number of columns in the table + * + * @see javax.swing.table.AbstractTableModel#getColumnCount + */ + public int getColumnCount() + { + // 3 columns: "name", "primary shortcut", "second shortcut" + return 3; + } + + /** + * Returns the text for the given cell of the table + * + * @param row cell row + * @param column cell column + * @return object at the row/column + * @see javax.swing.table.AbstractTableModel#getValueAt + */ + public Object getValueAt(int row, int column) + { + switch(column) + { + case 0: + return getEntryAt(row).getAction(); + case 1: + return GlobalShortcutEntry.getShortcutText( + getEntryAt(row).getShortcut()); + case 2: + return GlobalShortcutEntry.getShortcutText( + getEntryAt(row).getShortcut2()); + default: + throw new IllegalArgumentException("column not found"); + } + } + + /** + * Returns the LdapDirectory at the row 'row' + * + * @param row the row on which to find the LdapDirectory + * + * @return the LdapDirectory found + */ + public GlobalShortcutEntry getEntryAt(int row) + { + int i = 0; + + for(GlobalShortcutEntry entry : shortcuts) + { + if(i == row) + return entry; + i++; + } + + throw new IllegalArgumentException("row not found"); + } + + /** + * Returns whether a cell is editable. + * @param row row of the cell + * @param col column of the cell + * + * @return whether the cell is editable + */ + public boolean isCellEditable(int row, int col) + { + return false; + } + + /** + * Overrides a method that always returned Object.class + * Now it will return Boolean.class for the first method, + * letting the DefaultTableCellRenderer create checkboxes. + * + * @param columnIndex index of the column + * @return Column class + */ + public Class getColumnClass(int columnIndex) + { + Object o = getValueAt(0, columnIndex); + if(o == null) + return String.class; + return o.getClass(); + } + + /** + * Sets a value in an editable cell. + * + * @param aValue value to set + * @param rowIndex row index + * @param columnIndex column index + */ + public void setValueAt(Object aValue, int rowIndex, int columnIndex) + { + if(columnIndex != 0) + throw new IllegalArgumentException("non editable column!"); + } + + /** + * Adds an entry. + * + * @param entry entry to add + */ + public void addEntry(GlobalShortcutEntry entry) + { + shortcuts.add(entry); + } + + /** + * Adds an entry. + * + * @param entry entry to add + */ + public void removeEntry(GlobalShortcutEntry entry) + { + shortcuts.remove(entry); + } + + /** + * Returns all shortcuts. + * + * @return all shortcuts. + */ + public List getEntries() + { + return shortcuts; + } +} diff --git a/src/net/java/sip/communicator/plugin/keybindingchooser/globalchooser/Resources.java b/src/net/java/sip/communicator/plugin/keybindingchooser/globalchooser/Resources.java new file mode 100644 index 000000000..3b83b718d --- /dev/null +++ b/src/net/java/sip/communicator/plugin/keybindingchooser/globalchooser/Resources.java @@ -0,0 +1,43 @@ +/* + * Jitsi, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. + * See terms of license at gnu.org. + */ +package net.java.sip.communicator.plugin.keybindingchooser.globalchooser; + +import net.java.sip.communicator.plugin.keybindingchooser.*; + +/** + * The Resources class manages the access to the internationalization + * properties files and the image resources used in this plugin. + * + * @author Yana Stamcheva + */ +public class Resources +{ + /** + * Returns an internationalized string corresponding to the given key. + * + * @param key The key of the string. + * @return An internationalized string corresponding to the given key. + */ + public static String getString(String key) + { + return KeybindingChooserActivator.getResources() + .getI18NString(key); + } + + /** + * Returns an internationalized string corresponding to the given key. + * + * @param key The key of the string. + * @param params additionnal parameters + * @return An internationalized string corresponding to the given key. + */ + public static String getString(String key, String[] params) + { + return KeybindingChooserActivator.getResources() + .getI18NString(key, params); + } +} diff --git a/src/net/java/sip/communicator/plugin/keybindingchooser/keybindingChooser.manifest.mf b/src/net/java/sip/communicator/plugin/keybindingchooser/keybindingChooser.manifest.mf index 32f57fdd5..2746eea74 100644 --- a/src/net/java/sip/communicator/plugin/keybindingchooser/keybindingChooser.manifest.mf +++ b/src/net/java/sip/communicator/plugin/keybindingchooser/keybindingChooser.manifest.mf @@ -8,7 +8,10 @@ Import-Package: org.osgi.framework, net.java.sip.communicator.service.gui, net.java.sip.communicator.service.keybindings, net.java.sip.communicator.service.resources, + net.java.sip.communicator.service.configuration, + net.java.sip.communicator.service.globalshortcut, net.java.sip.communicator.util, net.java.sip.communicator.util.swing, javax.swing, - javax.swing.event + javax.swing.event, + javax.swing.table, diff --git a/src/net/java/sip/communicator/service/globalshortcut/GlobalShortcutEvent.java b/src/net/java/sip/communicator/service/globalshortcut/GlobalShortcutEvent.java new file mode 100644 index 000000000..3f613eef7 --- /dev/null +++ b/src/net/java/sip/communicator/service/globalshortcut/GlobalShortcutEvent.java @@ -0,0 +1,42 @@ +/* + * Jitsi, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. + * See terms of license at gnu.org. + */ +package net.java.sip.communicator.service.globalshortcut; + +import java.awt.*; + +/** + * Event related to global shortcut. + * + * @author Sebastien Vincent + */ +public class GlobalShortcutEvent +{ + /** + * Key stroke. + */ + private final AWTKeyStroke keyStroke; + + /** + * Initializes a new GlobalShortcutEvent. + * + * @param keyStroke keystroke + */ + public GlobalShortcutEvent(AWTKeyStroke keyStroke) + { + this.keyStroke = keyStroke; + } + + /** + * Returns keyStroke. + * + * @return keystroke + */ + public AWTKeyStroke getKeyStroke() + { + return keyStroke; + } +} diff --git a/src/net/java/sip/communicator/service/globalshortcut/GlobalShortcutListener.java b/src/net/java/sip/communicator/service/globalshortcut/GlobalShortcutListener.java new file mode 100644 index 000000000..9a372ce5b --- /dev/null +++ b/src/net/java/sip/communicator/service/globalshortcut/GlobalShortcutListener.java @@ -0,0 +1,22 @@ +/* + * Jitsi, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. + * See terms of license at gnu.org. + */ +package net.java.sip.communicator.service.globalshortcut; + +/** + * Global shortcut listener. + * + * @author Sebastien Vincent + */ +public interface GlobalShortcutListener +{ + /** + * Callback when an shortcut is typed + * + * @param evt GlobalShortcutEvent + */ + public void shortcutReceived(GlobalShortcutEvent evt); +} diff --git a/src/net/java/sip/communicator/service/globalshortcut/GlobalShortcutService.java b/src/net/java/sip/communicator/service/globalshortcut/GlobalShortcutService.java new file mode 100644 index 000000000..17f3e77fe --- /dev/null +++ b/src/net/java/sip/communicator/service/globalshortcut/GlobalShortcutService.java @@ -0,0 +1,41 @@ +/* + * Jitsi, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. + * See terms of license at gnu.org. + */ +package net.java.sip.communicator.service.globalshortcut; + +import java.awt.*; + +/** + * This global shortcut service permits to register listeners for global + * shortcut (i.e. keystroke even if application is not foreground). + * + * @author Sebastien Vincent + */ +public interface GlobalShortcutService +{ + /** + * Registers an action to execute when the keystroke is typed. + * + * @param l listener to notify when keystroke is typed + * @param keyStroke keystroke that will trigger the action + */ + public void registerShortcut(GlobalShortcutListener l, + AWTKeyStroke keyStroke); + + /** + * Unregisters an action to execute when the keystroke is typed. + * + * @param l listener to remove + * @param keyStroke keystroke that will trigger the action + */ + public void unregisterShortcut(GlobalShortcutListener l, + AWTKeyStroke keyStroke); + + /** + * Reload global shortcuts. + */ + public void reloadGlobalShortcuts(); +} \ No newline at end of file diff --git a/src/net/java/sip/communicator/service/keybindings/GlobalKeybindingSet.java b/src/net/java/sip/communicator/service/keybindings/GlobalKeybindingSet.java new file mode 100644 index 000000000..25d266839 --- /dev/null +++ b/src/net/java/sip/communicator/service/keybindings/GlobalKeybindingSet.java @@ -0,0 +1,33 @@ +/* + * Jitsi, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. + * See terms of license at gnu.org. + */ +package net.java.sip.communicator.service.keybindings; + +import java.util.*; +import java.util.List; //disambiguation +import java.awt.*; + +/** + * Global keybinding set interface. + * + * @author Sebastien Vincent + */ +public interface GlobalKeybindingSet +{ + /** + * Provides current keybinding mappings. + * @return mapping of keystrokes to the string representation of the actions + * they perform + */ + public Map> getBindings(); + + /** + * Resets the bindings and notifies the observer's listeners if they've + * changed. + * @param bindings new keybindings to be held + */ + public void setBindings(Map> bindings); +} diff --git a/src/net/java/sip/communicator/service/keybindings/KeybindingSet.java b/src/net/java/sip/communicator/service/keybindings/KeybindingSet.java index 7fd31626c..87085fb22 100644 --- a/src/net/java/sip/communicator/service/keybindings/KeybindingSet.java +++ b/src/net/java/sip/communicator/service/keybindings/KeybindingSet.java @@ -23,20 +23,20 @@ public abstract class KeybindingSet * they perform */ public abstract HashMap getBindings(); - + /** * Resets the bindings and notifies the observer's listeners if they've * changed. * @param newBindings new keybindings to be held */ public abstract void setBindings(Map newBindings); - + /** * Provides the portion of the UI to which the bindings belong. * @return binding category */ public abstract Category getCategory(); - + /** * Keybinding sets available in the Sip Communicator. */ @@ -44,16 +44,16 @@ public enum Category { CHAT("keybindings-chat", Persistence.SERIAL_HASH), MAIN("keybindings-main", Persistence.SERIAL_HASH); - + private final String resource; private final Persistence persistenceFormat; - + Category(String resource, Persistence format) { this.resource = resource; this.persistenceFormat = format; } - + /** * Provides the name keybindings are saved and loaded with. * @return filename used for keybindings @@ -62,7 +62,7 @@ public String getResource() { return this.resource; } - + /** * Provides the format used to save and load keybinding resources. * @return style of persistence used by keybindings diff --git a/src/net/java/sip/communicator/service/keybindings/KeybindingsService.java b/src/net/java/sip/communicator/service/keybindings/KeybindingsService.java index 335c84ddb..afc382565 100644 --- a/src/net/java/sip/communicator/service/keybindings/KeybindingsService.java +++ b/src/net/java/sip/communicator/service/keybindings/KeybindingsService.java @@ -6,6 +6,10 @@ */ package net.java.sip.communicator.service.keybindings; +import java.awt.*; +import java.util.*; +import java.util.List; + /** * The KeybindingService handles the distribution of configurable and * persistent keybinding sets. @@ -21,4 +25,23 @@ public interface KeybindingsService * actions */ KeybindingSet getBindings(KeybindingSet.Category category); + + /** + * Provides the bindings associated with the global category. + * + * @return global keybinding set + */ + GlobalKeybindingSet getGlobalBindings(); + + /** + * Returns list of global shortcuts from the configuration file. + * + * @return list of global shortcuts. + */ + public Map> getGlobalShortcutFromConfiguration(); + + /** + * Save the configuration file. + */ + public void saveGlobalShortcutFromConfiguration(); } \ No newline at end of file