add toggle for global/non-global hotkeys. add hook registration retry

kind of a hacky implementation, but it works.

ideally i would like to not depend on JNativeHook at all if global
hotkeys are disabled and provide some sort of "dual" event processing
using both the built in Swing key press events and the native key press
events, using input from whichever is appropriate based on the user
settings.

however, JNativeHook and Swing use different key codes. it would be
possible to write some code to translate between the two, but i don't
have a bunch of different keyboards and OS installs to test this
properly and feel that the odds of me writing some key code translation
function that doesn't work 100% of the time is a bit too high.

so, we just use JNativeHook for global and non-global and test for
window focus depending on which is enabled. this has the unfortunate
consequence of requiring that the key event hook registration was
successful regardless of if the user wants to use global or non-global
hotkeys. kind of annoying, but can't be helped for now!
This commit is contained in:
Gered 2015-12-02 12:52:49 -05:00
parent 3e460f4a12
commit 20bd866eba
5 changed files with 126 additions and 36 deletions

View file

@ -42,6 +42,7 @@ public enum Language {
setting_color_separators, setting_color_separators,
// Settings > Hotkey // Settings > Hotkey
setting_useGlobalHotkeys,
setting_hotkey_split, setting_hotkey_split,
setting_hotkey_unsplit, setting_hotkey_unsplit,
setting_hotkey_skip, setting_hotkey_skip,
@ -241,7 +242,13 @@ public enum Language {
* 1.4 * 1.4
*/ */
INCREMENT, INCREMENT,
START_VALUE; START_VALUE,
/* */
GLOBAL_HOTKEYS_WARNING,
GLOBAL_HOTKEYS_HOOK_RETRY,
GLOBAL_HOTKEYS_HOOK_ERROR;
public static final Locale[] LANGUAGES = new Locale[] { public static final Locale[] LANGUAGES = new Locale[] {
Locale.ENGLISH, Locale.ENGLISH,

View file

@ -284,15 +284,17 @@ public class Llanfair extends BorderlessFrame implements TableModelListener,
* main thread. * main thread.
*/ */
@Override public void nativeKeyPressed( final NativeKeyEvent event ) { @Override public void nativeKeyPressed( final NativeKeyEvent event ) {
int keyCode = event.getKeyCode(); if (Settings.useGlobalHotkeys.get() || this.hasFocus()) {
boolean hotkeysEnabler = ( keyCode == Settings.hotkeyLock.get() ); int keyCode = event.getKeyCode();
boolean hotkeysEnabler = ( keyCode == Settings.hotkeyLock.get() );
if ( !ignoresNativeInputs() || hotkeysEnabler ) { if ( !ignoresNativeInputs() || hotkeysEnabler ) {
SwingUtilities.invokeLater( new Runnable() { SwingUtilities.invokeLater( new Runnable() {
@Override public void run() { @Override public void run() {
actions.process( event ); actions.process( event );
} }
} ); } );
}
} }
} }
@ -448,15 +450,7 @@ public class Llanfair extends BorderlessFrame implements TableModelListener,
* @throws IllegalStateException if JNativeHook cannot be registered. * @throws IllegalStateException if JNativeHook cannot be registered.
*/ */
private void setBehavior() { private void setBehavior() {
try { registerNativeKeyHook();
GlobalScreen.registerNativeHook();
} catch (NativeHookException e) {
// NOTE: commenting this out as the latest version of JNativeHook has at least some ability to
// pop up an OS-specific dialog asking about accessibility permissions (at least on OS X)
// and afterwards the application recovered fine from the user's perspective. throwing an
// exception here causes Llanfair to just close immediately after the dialog has opened.
//throw new IllegalStateException("cannot register native hook");
}
setAlwaysOnTop(Settings.alwaysOnTop.get()); setAlwaysOnTop(Settings.alwaysOnTop.get());
addWindowListener(this); addWindowListener(this);
addMouseWheelListener(this); addMouseWheelListener(this);
@ -473,4 +467,21 @@ public class Llanfair extends BorderlessFrame implements TableModelListener,
MenuItem.populateRecentlyOpened(); MenuItem.populateRecentlyOpened();
} }
/**
* Attempts to register a hook to capture system-wide (global) key events.
* @return true if the hook was registered, false if not
*/
public static boolean registerNativeKeyHook() {
try {
GlobalScreen.registerNativeHook();
return true;
} catch (NativeHookException e) {
// NOTE: in the event of a failure, JNativeHook now has some ability (on some OS's at least)
// to pop up an OS-specific dialog or other action that allows the user to rectify the
// problem. e.g. on OS X, if an exception is thrown a dialog telling the user that the
// application has requested some accessibility-related access shows up.
return false;
}
}
} }

View file

@ -53,6 +53,7 @@ public class Settings {
/* HOTKEY properties */ /* HOTKEY properties */
public static final Property<Boolean> useGlobalHotkeys = new Property<>("useGlobalHotkeys");
public static final Property<Integer> hotkeySplit = new Property<>( "hotkey.split" ); public static final Property<Integer> hotkeySplit = new Property<>( "hotkey.split" );
public static final Property<Integer> hotkeyUnsplit = new Property<>( "hotkey.unsplit" ); public static final Property<Integer> hotkeyUnsplit = new Property<>( "hotkey.unsplit" );
public static final Property<Integer> hotkeySkip = new Property<>( "hotkey.skip" ); public static final Property<Integer> hotkeySkip = new Property<>( "hotkey.skip" );
@ -213,6 +214,7 @@ public class Settings {
global.put( colorHighlight.key, Color.decode( "0xffffff" ) ); global.put( colorHighlight.key, Color.decode( "0xffffff" ) );
global.put( colorSeparators.key, Color.decode( "0x666666" ) ); global.put( colorSeparators.key, Color.decode( "0x666666" ) );
global.put( useGlobalHotkeys.key, false );
global.put( hotkeySplit.key, -1 ); global.put( hotkeySplit.key, -1 );
global.put( hotkeyUnsplit.key, -1 ); global.put( hotkeyUnsplit.key, -1 );
global.put( hotkeySkip.key, -1 ); global.put( hotkeySkip.key, -1 );

View file

@ -1,6 +1,7 @@
package org.fenix.llanfair.dialog; package org.fenix.llanfair.dialog;
import org.fenix.llanfair.Language; import org.fenix.llanfair.Language;
import org.fenix.llanfair.Llanfair;
import org.fenix.llanfair.config.Settings; import org.fenix.llanfair.config.Settings;
import org.fenix.utils.gui.GBC; import org.fenix.utils.gui.GBC;
import org.jnativehook.GlobalScreen; import org.jnativehook.GlobalScreen;
@ -9,8 +10,7 @@ import org.jnativehook.keyboard.NativeKeyListener;
import javax.swing.*; import javax.swing.*;
import java.awt.*; import java.awt.*;
import java.awt.event.MouseEvent; import java.awt.event.*;
import java.awt.event.MouseListener;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -22,6 +22,11 @@ class TabHotkeys extends SettingsTab {
// ------------------------------------------------------------- ATTRIBUTES // ------------------------------------------------------------- ATTRIBUTES
private JCheckBox globalHotKeys;
private JLabel globalHotKeysHookWarning;
private JButton globalHotKeysHookRetryButton;
/** /**
* List of all key fields customizable by the user. * List of all key fields customizable by the user.
*/ */
@ -38,6 +43,31 @@ class TabHotkeys extends SettingsTab {
* Creates the "Hotkeys" settings tab. Only called by {@link EditSettings}. * Creates the "Hotkeys" settings tab. Only called by {@link EditSettings}.
*/ */
TabHotkeys() { TabHotkeys() {
final Component that = this;
globalHotKeys = new JCheckBox("" + Language.setting_useGlobalHotkeys);
globalHotKeys.setSelected(Settings.useGlobalHotkeys.get());
globalHotKeys.addActionListener(new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) { Settings.useGlobalHotkeys.set(globalHotKeys.isSelected()); }
});
globalHotKeysHookWarning = new JLabel("" + Language.GLOBAL_HOTKEYS_WARNING);
globalHotKeysHookWarning.setForeground(Color.RED);
globalHotKeysHookRetryButton = new JButton("" + Language.GLOBAL_HOTKEYS_HOOK_RETRY);
globalHotKeysHookRetryButton.addActionListener(new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
boolean isRegistered = Llanfair.registerNativeKeyHook();
if (isRegistered) {
globalHotKeysHookWarning.setVisible(false);
globalHotKeysHookRetryButton.setVisible(false);
} else {
JOptionPane.showMessageDialog(that, Language.GLOBAL_HOTKEYS_HOOK_ERROR, Language.ERROR.get(), JOptionPane.ERROR_MESSAGE);
}
}
});
keyFields = new ArrayList<KeyField>(); keyFields = new ArrayList<KeyField>();
keyLabels = new ArrayList<JLabel>(); keyLabels = new ArrayList<JLabel>();
@ -72,13 +102,21 @@ class TabHotkeys extends SettingsTab {
private void place() { private void place() {
setLayout(new GridBagLayout()); setLayout(new GridBagLayout());
for (int row = 0; row < keyFields.size(); row++) { int row;
for (row = 0; row < keyFields.size(); row++) {
add( add(
keyLabels.get(row), keyLabels.get(row),
GBC.grid(0, row).insets(10, 0, 10, 10).anchor(GBC.LE) GBC.grid(0, row).insets(10, 0, 10, 10).anchor(GBC.LE)
); );
add(keyFields.get(row), GBC.grid(1, row)); add(keyFields.get(row), GBC.grid(1, row));
} }
add(globalHotKeys, GBC.grid(2, 3).insets(0, 50, 0, 0).anchor(GBC.LS));
if (!GlobalScreen.isNativeHookRegistered()) {
add(globalHotKeysHookWarning, GBC.grid(0, row + 1, 3, 1).insets(10, 0, 10, 0));
add(globalHotKeysHookRetryButton, GBC.grid(0, row + 2, 3, 1).insets(0, 0, 10, 0));
}
} }
// --------------------------------------------------------- INTERNAL TYPES // --------------------------------------------------------- INTERNAL TYPES
@ -91,7 +129,9 @@ class TabHotkeys extends SettingsTab {
* @author Xavier "Xunkar" Sencert * @author Xavier "Xunkar" Sencert
*/ */
static class KeyField extends JTextField static class KeyField extends JTextField
implements MouseListener, NativeKeyListener { implements MouseListener, FocusListener, NativeKeyListener {
private Color originalBgColor;
// ----------------------------------------------------- CONSTANTS // ----------------------------------------------------- CONSTANTS
@ -134,7 +174,10 @@ class TabHotkeys extends SettingsTab {
String text = NativeKeyEvent.getKeyText(setting.get()); String text = NativeKeyEvent.getKeyText(setting.get());
setText(setting.get() == -1 ? "" + Language.DISABLED : text); setText(setting.get() == -1 ? "" + Language.DISABLED : text);
originalBgColor = getBackground();
addMouseListener(this); addMouseListener(this);
addFocusListener(this);
} }
// ------------------------------------------------------- GETTERS // ------------------------------------------------------- GETTERS
@ -146,6 +189,33 @@ class TabHotkeys extends SettingsTab {
return setting; return setting;
} }
// ----------------------------------------------------- CORE KEY EVENT HANDLING
private void enableKeyListening(boolean enable) {
if (enable) {
if (!isEditing) {
setBackground(Color.YELLOW);
GlobalScreen.addNativeKeyListener(this);
isEditing = true;
}
} else {
if (isEditing) {
setBackground(originalBgColor);
GlobalScreen.removeNativeKeyListener(this);
isEditing = false;
}
}
}
private void setKey(int code, String keyText) {
setting.set(code);
if (code == -1)
setText("" + Language.DISABLED);
else
setText(keyText);
}
// ----------------------------------------------------- CALLBACKS // ----------------------------------------------------- CALLBACKS
/** /**
@ -154,11 +224,7 @@ class TabHotkeys extends SettingsTab {
* new color to signify that this field is now listening. * new color to signify that this field is now listening.
*/ */
public void mouseClicked(MouseEvent event) { public void mouseClicked(MouseEvent event) {
if (!isEditing) { enableKeyListening(true);
setBackground(Color.YELLOW);
GlobalScreen.addNativeKeyListener(this);
isEditing = true;
}
} }
// $UNUSED$ // $UNUSED$
@ -183,18 +249,13 @@ class TabHotkeys extends SettingsTab {
int code = event.getKeyCode(); int code = event.getKeyCode();
String text = null; String text = null;
if (code == NativeKeyEvent.VC_ESCAPE) { if (code == NativeKeyEvent.VC_ESCAPE)
code = -1; code = -1;
text = "" + Language.DISABLED; else
} else {
text = NativeKeyEvent.getKeyText(code); text = NativeKeyEvent.getKeyText(code);
}
setText(text);
setting.set(code);
setBackground(Color.GREEN); setKey(code, text);
GlobalScreen.removeNativeKeyListener(this); enableKeyListening(false);
isEditing = false;
} }
// $UNUSED$ // $UNUSED$
@ -203,5 +264,10 @@ class TabHotkeys extends SettingsTab {
// $UNUSED$ // $UNUSED$
public void nativeKeyTyped(NativeKeyEvent event) {} public void nativeKeyTyped(NativeKeyEvent event) {}
public void focusLost(FocusEvent event) {
enableKeyListening(false);
}
public void focusGained(FocusEvent event) {}
} }
} }

View file

@ -18,6 +18,7 @@ setting_color_newRecord = New Record
setting_color_title = Title setting_color_title = Title
setting_color_highlight = Highlight setting_color_highlight = Highlight
setting_color_separators = Separators setting_color_separators = Separators
setting_useGlobalHotkeys = Global Hotkeys
setting_hotkey_split = Start / Split setting_hotkey_split = Start / Split
setting_hotkey_unsplit = Unsplit setting_hotkey_unsplit = Unsplit
setting_hotkey_skip = Skip setting_hotkey_skip = Skip
@ -180,3 +181,6 @@ UNTITLED = <untitled>
WARNING = Warning WARNING = Warning
INCREMENT = INCREMENT =
START_VALUE = START_VALUE =
GLOBAL_HOTKEYS_WARNING = <html><div style="width: 300px;">Key event hook registration failed. <strong>You will not be able to set or use any of your hotkeys until this is fixed!</strong> Click 'Retry' below to attempt to register it again.</div></html>
GLOBAL_HOTKEYS_HOOK_RETRY = Retry
GLOBAL_HOTKEYS_HOOK_ERROR = Key event hook registration failed. You will need to grant extra accessibility permissions to Llanfair.