001package org.jdesktop.swingx.plaf.basic;
002
003import static java.awt.event.KeyEvent.VK_CAPS_LOCK;
004
005import java.awt.KeyEventDispatcher;
006import java.awt.KeyboardFocusManager;
007import java.awt.Toolkit;
008import java.awt.event.KeyEvent;
009
010import org.jdesktop.beans.AbstractBean;
011
012/**
013 * A class for determining the state of the {@link java.awt.event.KeyEvent.VK_CAPS_LOCK CAPS LOCK
014 * key}. It also supports notification when the locking state changes.
015 * <p>
016 * Although it is possible to use {@link Toolkit#getLockingKeyState(int)} to determine the current
017 * state of the CAPS LOCK key, that method is not guaranteed to work on all platforms. This class
018 * attempts to handle those shortfalls and provide an easy mechanism for listening to state changes.
019 * 
020 * <pre>
021 * CapsLockSupport cls = CapsLockSupport.getInstance();
022 * // for get the current state of the caps lock key
023 * boolean currentState = cls.isCapsLockEnabled();
024 * // for listening to changes in the caps lock state
025 * cls.addPropertyChangeListener(&quot;capsLockEnabled&quot;, myListener);
026 * </pre>
027 * 
028 * There is one special case to be aware of. If {@code CapsLockSupport} is not able to determine the
029 * state of the CAPS LOCK key, then {@link #isInitialized()} will return {@code false} until it is
030 * able to introspect a {@link KeyEvent} and determine the current locking state. If
031 * {@code CapsLockSupport} must use delayed initialization, it will fire a property change to notify
032 * listeners that it is now in an accurate state.
033 * 
034 * @author kschaefer
035 */
036public final class CapsLockSupport extends AbstractBean implements KeyEventDispatcher {
037    private boolean useToolkit;
038    private boolean capsLockeEnabled;
039    private boolean updateViaKeyEvent;
040    
041    private static class SingletonHolder {
042        private static final CapsLockSupport INSTANCE = new CapsLockSupport();
043        
044        static {
045            KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher(INSTANCE);
046        }
047    }
048    
049    private CapsLockSupport() {
050        try {
051            capsLockeEnabled = Toolkit.getDefaultToolkit().getLockingKeyState(VK_CAPS_LOCK);
052            useToolkit = true;
053            updateViaKeyEvent = false;
054        } catch (UnsupportedOperationException e) {
055            capsLockeEnabled = false;
056            useToolkit = false;
057            updateViaKeyEvent = true;
058        }
059    }
060    
061    /**
062     * Gets the only instance of {@code CapsLockSupport}.
063     * 
064     * @return the {@code CapsLockSupport} instance
065     */
066    public static CapsLockSupport getInstance() {
067        return SingletonHolder.INSTANCE;
068    }
069
070    /**
071     * Determines if {@code CapsLockSupport} is accurately synchronized with the state of the CAPS
072     * LOCK key. When not initialized, {@link #isCapsLockEnabled()} will always return {@code false}
073     * . {@code CapsLockSupport} will fail to initialize only if
074     * {@code Toolkit#getLockingKeyState(int)} throws an exception; in that case, it will initialize
075     * as soon as it receives a valid key event (that can be used to determine the current locking
076     * state).
077     * 
078     * @return {@code true} if {@code CapsLockSupport} accurately knows the state of the CAPS LOCK
079     *         key
080     */
081    public boolean isInitialized() {
082        return useToolkit || (useToolkit ^ updateViaKeyEvent);
083    }
084    
085    /**
086     * Determines the current state of the {@link java.awt.event.KeyEvent.VK_CAPS_LOCK CAPS LOCK key}.
087     * 
088     * @return {@code true} if CAPS LOCK is enabled; {@code false} otherwise
089     */
090    public boolean isCapsLockEnabled() {
091        if (useToolkit) {
092            try {
093                return Toolkit.getDefaultToolkit().getLockingKeyState(VK_CAPS_LOCK);
094            } catch (UnsupportedOperationException shouldNeverHappen) {
095                return capsLockeEnabled;
096            }
097        }
098        
099        return capsLockeEnabled;
100    }
101    
102    void setCapsLockEnabled(boolean capsLockEnabled) {
103        boolean oldValue = this.capsLockeEnabled;
104        this.capsLockeEnabled = capsLockEnabled;
105        firePropertyChange("capsLockEnabled", oldValue, this.capsLockeEnabled); //$NON-NLS-1$
106    }
107
108    // updateViaKeyEvent is use to find the initial state of the CAPS LOCK key when the Toolkit does
109    // not support it
110    /**
111     * This is an implementation detail and should not be considered public.
112     */
113    @Override
114    public boolean dispatchKeyEvent(KeyEvent e) {
115        if (e.getID() == KeyEvent.KEY_PRESSED) {
116            int keyCode = e.getKeyCode();
117            
118            if (keyCode == VK_CAPS_LOCK) {
119                if (!updateViaKeyEvent) {
120                    if (useToolkit) {
121                        try {
122                            setCapsLockEnabled(Toolkit.getDefaultToolkit().getLockingKeyState(VK_CAPS_LOCK));
123                        } catch (UnsupportedOperationException shouldNeverHappen) {
124                            setCapsLockEnabled(!capsLockeEnabled);
125                        }
126                    } else {
127                        setCapsLockEnabled(!capsLockeEnabled);
128                    }
129                }
130            } else if (updateViaKeyEvent && Character.isLetter(keyCode)) {
131                if (keyCode == e.getKeyChar()) {
132                    capsLockeEnabled = !e.isShiftDown();
133                } else {
134                    capsLockeEnabled = e.isShiftDown();
135                }
136                
137                updateViaKeyEvent = false;
138                firePropertyChange("initialized", false, true); //$NON-NLS-1$
139            }
140        }
141        
142        return false;
143    }
144}