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