From 20bd866ebaed797a86d426c40db26137f88e20fc Mon Sep 17 00:00:00 2001 From: gered Date: Wed, 2 Dec 2015 12:52:49 -0500 Subject: [PATCH] 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! --- .../java/org/fenix/llanfair/Language.java | 9 +- .../java/org/fenix/llanfair/Llanfair.java | 45 +++++--- .../org/fenix/llanfair/config/Settings.java | 2 + .../org/fenix/llanfair/dialog/TabHotkeys.java | 102 ++++++++++++++---- src/main/resources/language.properties | 4 + 5 files changed, 126 insertions(+), 36 deletions(-) diff --git a/src/main/java/org/fenix/llanfair/Language.java b/src/main/java/org/fenix/llanfair/Language.java index dd3d2e1..068eb5c 100644 --- a/src/main/java/org/fenix/llanfair/Language.java +++ b/src/main/java/org/fenix/llanfair/Language.java @@ -42,6 +42,7 @@ public enum Language { setting_color_separators, // Settings > Hotkey + setting_useGlobalHotkeys, setting_hotkey_split, setting_hotkey_unsplit, setting_hotkey_skip, @@ -241,7 +242,13 @@ public enum Language { * 1.4 */ INCREMENT, - START_VALUE; + START_VALUE, + + + /* */ + GLOBAL_HOTKEYS_WARNING, + GLOBAL_HOTKEYS_HOOK_RETRY, + GLOBAL_HOTKEYS_HOOK_ERROR; public static final Locale[] LANGUAGES = new Locale[] { Locale.ENGLISH, diff --git a/src/main/java/org/fenix/llanfair/Llanfair.java b/src/main/java/org/fenix/llanfair/Llanfair.java index 14915c5..e1b4be8 100644 --- a/src/main/java/org/fenix/llanfair/Llanfair.java +++ b/src/main/java/org/fenix/llanfair/Llanfair.java @@ -284,15 +284,17 @@ public class Llanfair extends BorderlessFrame implements TableModelListener, * main thread. */ @Override public void nativeKeyPressed( final NativeKeyEvent event ) { - int keyCode = event.getKeyCode(); - boolean hotkeysEnabler = ( keyCode == Settings.hotkeyLock.get() ); + if (Settings.useGlobalHotkeys.get() || this.hasFocus()) { + int keyCode = event.getKeyCode(); + boolean hotkeysEnabler = ( keyCode == Settings.hotkeyLock.get() ); - if ( !ignoresNativeInputs() || hotkeysEnabler ) { - SwingUtilities.invokeLater( new Runnable() { - @Override public void run() { - actions.process( event ); - } - } ); + if ( !ignoresNativeInputs() || hotkeysEnabler ) { + SwingUtilities.invokeLater( new Runnable() { + @Override public void run() { + actions.process( event ); + } + } ); + } } } @@ -448,15 +450,7 @@ public class Llanfair extends BorderlessFrame implements TableModelListener, * @throws IllegalStateException if JNativeHook cannot be registered. */ private void setBehavior() { - try { - 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"); - } + registerNativeKeyHook(); setAlwaysOnTop(Settings.alwaysOnTop.get()); addWindowListener(this); addMouseWheelListener(this); @@ -473,4 +467,21 @@ public class Llanfair extends BorderlessFrame implements TableModelListener, 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; + } + } + } diff --git a/src/main/java/org/fenix/llanfair/config/Settings.java b/src/main/java/org/fenix/llanfair/config/Settings.java index 8379b4d..d683867 100644 --- a/src/main/java/org/fenix/llanfair/config/Settings.java +++ b/src/main/java/org/fenix/llanfair/config/Settings.java @@ -53,6 +53,7 @@ public class Settings { /* HOTKEY properties */ + public static final Property useGlobalHotkeys = new Property<>("useGlobalHotkeys"); public static final Property hotkeySplit = new Property<>( "hotkey.split" ); public static final Property hotkeyUnsplit = new Property<>( "hotkey.unsplit" ); public static final Property hotkeySkip = new Property<>( "hotkey.skip" ); @@ -213,6 +214,7 @@ public class Settings { global.put( colorHighlight.key, Color.decode( "0xffffff" ) ); global.put( colorSeparators.key, Color.decode( "0x666666" ) ); + global.put( useGlobalHotkeys.key, false ); global.put( hotkeySplit.key, -1 ); global.put( hotkeyUnsplit.key, -1 ); global.put( hotkeySkip.key, -1 ); diff --git a/src/main/java/org/fenix/llanfair/dialog/TabHotkeys.java b/src/main/java/org/fenix/llanfair/dialog/TabHotkeys.java index a3e7b4d..0c23fb1 100644 --- a/src/main/java/org/fenix/llanfair/dialog/TabHotkeys.java +++ b/src/main/java/org/fenix/llanfair/dialog/TabHotkeys.java @@ -1,6 +1,7 @@ package org.fenix.llanfair.dialog; import org.fenix.llanfair.Language; +import org.fenix.llanfair.Llanfair; import org.fenix.llanfair.config.Settings; import org.fenix.utils.gui.GBC; import org.jnativehook.GlobalScreen; @@ -9,8 +10,7 @@ import org.jnativehook.keyboard.NativeKeyListener; import javax.swing.*; import java.awt.*; -import java.awt.event.MouseEvent; -import java.awt.event.MouseListener; +import java.awt.event.*; import java.util.ArrayList; import java.util.List; @@ -22,6 +22,11 @@ class TabHotkeys extends SettingsTab { // ------------------------------------------------------------- ATTRIBUTES + private JCheckBox globalHotKeys; + + private JLabel globalHotKeysHookWarning; + private JButton globalHotKeysHookRetryButton; + /** * 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}. */ 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(); keyLabels = new ArrayList(); @@ -72,13 +102,21 @@ class TabHotkeys extends SettingsTab { private void place() { setLayout(new GridBagLayout()); - for (int row = 0; row < keyFields.size(); row++) { + int row; + for (row = 0; row < keyFields.size(); row++) { add( keyLabels.get(row), GBC.grid(0, row).insets(10, 0, 10, 10).anchor(GBC.LE) ); 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 @@ -91,7 +129,9 @@ class TabHotkeys extends SettingsTab { * @author Xavier "Xunkar" Sencert */ static class KeyField extends JTextField - implements MouseListener, NativeKeyListener { + implements MouseListener, FocusListener, NativeKeyListener { + + private Color originalBgColor; // ----------------------------------------------------- CONSTANTS @@ -134,7 +174,10 @@ class TabHotkeys extends SettingsTab { String text = NativeKeyEvent.getKeyText(setting.get()); setText(setting.get() == -1 ? "" + Language.DISABLED : text); + originalBgColor = getBackground(); + addMouseListener(this); + addFocusListener(this); } // ------------------------------------------------------- GETTERS @@ -146,6 +189,33 @@ class TabHotkeys extends SettingsTab { 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 /** @@ -154,11 +224,7 @@ class TabHotkeys extends SettingsTab { * new color to signify that this field is now listening. */ public void mouseClicked(MouseEvent event) { - if (!isEditing) { - setBackground(Color.YELLOW); - GlobalScreen.addNativeKeyListener(this); - isEditing = true; - } + enableKeyListening(true); } // $UNUSED$ @@ -183,18 +249,13 @@ class TabHotkeys extends SettingsTab { int code = event.getKeyCode(); String text = null; - if (code == NativeKeyEvent.VC_ESCAPE) { + if (code == NativeKeyEvent.VC_ESCAPE) code = -1; - text = "" + Language.DISABLED; - } else { + else text = NativeKeyEvent.getKeyText(code); - } - setText(text); - setting.set(code); - setBackground(Color.GREEN); - GlobalScreen.removeNativeKeyListener(this); - isEditing = false; + setKey(code, text); + enableKeyListening(false); } // $UNUSED$ @@ -203,5 +264,10 @@ class TabHotkeys extends SettingsTab { // $UNUSED$ public void nativeKeyTyped(NativeKeyEvent event) {} + public void focusLost(FocusEvent event) { + enableKeyListening(false); + } + + public void focusGained(FocusEvent event) {} } } diff --git a/src/main/resources/language.properties b/src/main/resources/language.properties index fc6324e..eba3d4d 100644 --- a/src/main/resources/language.properties +++ b/src/main/resources/language.properties @@ -18,6 +18,7 @@ setting_color_newRecord = New Record setting_color_title = Title setting_color_highlight = Highlight setting_color_separators = Separators +setting_useGlobalHotkeys = Global Hotkeys setting_hotkey_split = Start / Split setting_hotkey_unsplit = Unsplit setting_hotkey_skip = Skip @@ -180,3 +181,6 @@ UNTITLED = WARNING = Warning INCREMENT = START_VALUE = +GLOBAL_HOTKEYS_WARNING =
Key event hook registration failed. You will not be able to set or use any of your hotkeys until this is fixed! Click 'Retry' below to attempt to register it again.
+GLOBAL_HOTKEYS_HOOK_RETRY = Retry +GLOBAL_HOTKEYS_HOOK_ERROR = Key event hook registration failed. You will need to grant extra accessibility permissions to Llanfair. \ No newline at end of file