/*
* Jitsi, the OpenSource Java VoIP and Instant Messaging client.
*
* Copyright @ 2015 Atlassian Pty Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.java.sip.communicator.impl.globalshortcut;
import java.awt.*;
import java.util.*;
import java.util.List;
import net.java.sip.communicator.service.globalshortcut.*;
import net.java.sip.communicator.service.keybindings.*;
import net.java.sip.communicator.util.Logger;
import org.jitsi.util.*;
// disambiguation
/**
* 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>();
/**
* List of notifiers when special key detection is enabled.
*/
private final List specialKeyNotifiers =
new ArrayList();
/**
* 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();
/**
* Last special key detected.
*/
private AWTKeyStroke specialKeyDetected = null;
/**
* Object to synchronize special key detection.
*/
private final Object specialKeySyncRoot = new Object();
/**
* 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)
{
registerShortcut(listener, keyStroke, true);
}
/**
* 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
* @param add add the listener/keystrokes to map
*/
public void registerShortcut(GlobalShortcutListener listener,
AWTKeyStroke keyStroke, boolean add)
{
synchronized(mapActions)
{
List keystrokes = mapActions.get(listener);
boolean ok = false;
if(keyStroke == null)
{
return;
}
if(keystrokes == null)
{
keystrokes = new ArrayList();
}
if(keyStroke.getModifiers() != SPECIAL_KEY_MODIFIERS)
{
ok = keyboardHook.registerShortcut(keyStroke.getKeyCode(),
getModifiers(keyStroke), keyStroke.isOnKeyRelease());
}
else
{
ok = keyboardHook.registerSpecial(keyStroke.getKeyCode(),
keyStroke.isOnKeyRelease());
}
if(ok && add)
{
keystrokes.add(keyStroke);
}
if(add)
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)
{
unregisterShortcut(listener, keyStroke, true);
}
/**
* Unregisters an action to execute when the keystroke is typed.
*
* @param listener listener to remove
* @param keyStroke keystroke that will trigger the action
* @param remove remove or not entry in the map
*/
public void unregisterShortcut(GlobalShortcutListener listener,
AWTKeyStroke keyStroke, boolean remove)
{
synchronized(mapActions)
{
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(modifiers != SPECIAL_KEY_MODIFIERS)
{
keyboardHook.unregisterShortcut(
keyStroke.getKeyCode(),
getModifiers(keyStroke));
}
else
{
keyboardHook.unregisterSpecial(keyStroke.getKeyCode());
}
if(remove)
{
if(ks != null)
{
keystrokes.remove(ks);
}
if(keystrokes.size() == 0)
{
mapActions.remove(listener);
}
else
{
// We do not have to put keystrokes back into mapActions
// because keystrokes is a reference to a modifiable
// List.
}
}
}
}
}
/**
* Start the service.
*/
public void start()
{
if(!isRunning)
{
keyboardHook.setDelegate(this);
keyboardHook.start();
isRunning = true;
}
}
/**
* Stop the service.
*/
public void stop()
{
isRunning = false;
// FIXME Lyubomir Marinov: The method unregisterShortcut will cause a
// ConcurrentModificationException because of either mapActions or a
// List value. The method stop was never invoked before
// though because of a bug in the methods start and stop of the class
// GlobalShortcutActivator.
// for(Map.Entry> entry
// : mapActions.entrySet())
// {
// GlobalShortcutListener l = entry.getKey();
// for(AWTKeyStroke e : entry.getValue())
// {
// unregisterShortcut(l, 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, ...)
* @param isOnKeyRelease this parameter is true if the shortcut is released
*/
public synchronized void receiveKey(int keycode, int modifiers,
boolean onRelease)
{
if(keyboardHook.isSpecialKeyDetection())
{
specialKeyDetected = AWTKeyStroke.getAWTKeyStroke(keycode,
modifiers);
synchronized(specialKeySyncRoot)
{
specialKeySyncRoot.notify();
}
GlobalShortcutEvent evt = new GlobalShortcutEvent(
specialKeyDetected, onRelease);
List copyListeners =
new ArrayList(specialKeyNotifiers);
for(GlobalShortcutListener l : copyListeners)
{
l.shortcutReceived(evt);
}
// if special key detection is enabled, disable all other shortcuts
return;
}
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 ||
(modifiers == SPECIAL_KEY_MODIFIERS &&
l.getModifiers() == modifiers)))
{
// notify corresponding listeners
GlobalShortcutEvent evt = new GlobalShortcutEvent(
l, onRelease);
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 :
mapActions.entrySet())
{
GlobalShortcutListener l = entry.getKey();
for(AWTKeyStroke e : entry.getValue())
{
unregisterShortcut(l, e, false);
}
}
mapActions.clear();
// add shortcuts from configuration
for(Map.Entry> entry :
set.getBindings().entrySet())
{
if(entry.getKey().equals("answer") ||
entry.getKey().equals("hangup") ||
entry.getKey().equals("answer_hangup") ||
entry.getKey().equals("mute") ||
entry.getKey().equals("push_to_talk"))
{
for(AWTKeyStroke e : entry.getValue())
{
if(entry.getKey().equals("push_to_talk"))
{
if(e != null)
registerShortcut(callShortcut,
AWTKeyStroke.getAWTKeyStroke(
e.getKeyCode(), e.getModifiers(), true));
}
else
{
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;
}
/**
* Enable or not global shortcut.
*
* @param enable enable or not global shortcut
*/
public void setEnable(boolean enable)
{
if(mapActions.size() > 0)
{
if(enable)
{
for(Map.Entry> entry
: mapActions.entrySet())
{
GlobalShortcutListener l = entry.getKey();
for(AWTKeyStroke e : entry.getValue())
{
registerShortcut(l, e, false);
}
}
}
else
{
for(Map.Entry> entry
: mapActions.entrySet())
{
GlobalShortcutListener l = entry.getKey();
for(AWTKeyStroke e : entry.getValue())
{
unregisterShortcut(l, e, false);
}
}
}
}
}
/**
* Enable or disable special key detection.
*
* @param enable enable or not special key detection.
* @param callback object to be notified
*/
public synchronized void setSpecialKeyDetection(boolean enable,
GlobalShortcutListener callback)
{
keyboardHook.detectSpecialKeyPress(enable);
if(specialKeyNotifiers.contains(callback) == enable)
{
return;
}
if(enable)
{
specialKeyNotifiers.add(callback);
}
else
{
specialKeyNotifiers.remove(callback);
}
}
/**
* Get special keystroke or null if not supported or user cancels. If no
* special key is detected for 5 seconds, it returns null
*
* @return special keystroke or null if not supported or user cancels
*/
public AWTKeyStroke getSpecialKey()
{
AWTKeyStroke ret = null;
specialKeyDetected = null;
keyboardHook.detectSpecialKeyPress(true);
// Windows only for the moment
if(OSUtils.IS_WINDOWS)
{
synchronized(specialKeySyncRoot)
{
try
{
specialKeySyncRoot.wait(5000);
}
catch(InterruptedException e)
{
}
}
ret = specialKeyDetected;
specialKeyDetected = null;
}
keyboardHook.detectSpecialKeyPress(false);
return ret;
}
/**
* 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");
*/
}
}