In order to make my UIRobot more acceptable to the marketplace, I needed a way in which I could unobtrusively hook myself into the AWT event system. I wanted a hotkey that users of the UIRobot could employ from any window to open the UIRobot dialog. The user could then "record" a script, "play" a script or "edit" a script, much like the macro recorder in MS Office (TM), using text on the components to locate them again. The only thing I expect the client code to do is to call
Class.forName("com.maxoft.ui.robot.UIRobot");
in order to give the UIRobot class a chance to register itself. Once that happens, I want the hotkey to be available from any frame, dialog, component, focused or not, etc. It should be a global hotkey that you can press to activate the UIRobot dialog. I had a look at the EventQueue for my first newsletter and noticed that it followed, roughly, the Chain of Responsibility design pattern. You can register a new event queue and make it responsible for handling any new events that arrive.
I saw this pattern quite quickly, but it took me 3 hours (!) to finally get it working. Had I been more careful, it should have taken not more than 5 minutes, but sometimes I am a bit slow on the uptake. One of the disadvantages of having written too much generic Java code is that I don't recognise "private" methods as an obstacle, because I can just invoke them anyway. Designers of frameworks are not necessarily very skilled at guessing how I want to extend their framework so they often make a method private instead of protected. I even went so far as to decompile SUN's implementation of java.awt.Toolkit, namely sun.awt.SunToolkit, to try and find some way of hooking into the event queue. In the end the correct and most simple way of doing this was to write a subclass of EventQueue, called MyEventQueue, and to register it as the now reigning king of the Democratic Republic of Events with the command:
Toolkit.getDefaultToolkit().getSystemEventQueue().push(
new MyEventQueue());
The reason it took me 3 hours to figure these couple of lines out was because I overrode the postEvent() method, instead of the dispatchEvent() method, duh! An example of MyEventQueue could look like this: //: MyEventQueue.java import java.awt.*; import java.awt.event.*; import javax.swing.*; public class MyEventQueue extends EventQueue { protected void dispatchEvent(AWTEvent event) { // the only functionality I add is that I print out all the events System.out.println(event); super.dispatchEvent(event); } }So, what type of functionality can you achieve with this code? You can write a global hotkey manager, you can write a recorder that generates scripts for the UIRobot or you can disable all user actions while the GUI is busy with something else. Those of you who've tried to disable GUI input have probably used the GlassPane of the JFrame which can catch all mouse events, but not keyboard shortcuts. Thanks to David Geary for that idea in his classic book on Swing!
I mentioned to one of our system engineers the possibility of using this event queue mechanism as a global hotkey manager. He got very excited and called Herman away from the company month-end barbacue to come talk to me. We had been struggling to get application-wide global hotkeys working for 3.5 years in our application and one mayor customer site was holding back on a purchase because of that problem. Herman and I sat down and we came up with the GlobalHotkeyManager, where you can register any input / action combination and it matches the two for you on a global scale. Note that you need JDK 1.3 to use ActionMap and InputMap. The other parts work in JDK 1.2.
//: GlobalHotkeyManager.java import java.awt.*; import java.awt.event.*; import javax.swing.*; public class GlobalHotkeyManager extends EventQueue { private static final boolean DEBUG = true; // BUG? what's that? ;-)) private static final GlobalHotkeyManager instance = new GlobalHotkeyManager(); private final InputMap keyStrokes = new InputMap(); private final ActionMap actions = new ActionMap(); static { // here we register ourselves as a new link in the chain of // responsibility Toolkit.getDefaultToolkit().getSystemEventQueue().push(instance); } private GlobalHotkeyManager() {} // One is enough - singleton public static GlobalHotkeyManager getInstance() { return instance; } public InputMap getInputMap() { return keyStrokes; } public ActionMap getActionMap() { return actions; } protected void dispatchEvent(AWTEvent event) { if (event instanceof KeyEvent) { // KeyStroke.getKeyStrokeForEvent converts an ordinary KeyEvent // to a keystroke, as stored in the InputMap. Keep in mind that // Numpad keystrokes are different to ordinary keys, i.e. if you // are listening to KeyStroke ks = KeyStroke.getKeyStrokeForEvent((KeyEvent)event); if (DEBUG) System.out.println("KeyStroke=" + ks); String actionKey = (String)keyStrokes.get(ks); if (actionKey != null) { if (DEBUG) System.out.println("ActionKey=" + actionKey); Action action = actions.get(actionKey); if (action != null && action.isEnabled()) { // I'm not sure about the parameters action.actionPerformed( new ActionEvent(event.getSource(), event.getID(), actionKey, ((KeyEvent)event).getModifiers())); return; // consume event } } } super.dispatchEvent(event); // let the next in chain handle event } }Together with the GlobalHotkeyManager.java we have a test program:
//: GlobalHotkeyManagerTest.java import javax.swing.*; import java.awt.*; import java.awt.event.*; public class GlobalHotkeyManagerTest extends JFrame { private final static String UIROBOT_KEY = "UIRobot"; private final KeyStroke uirobotHotkey = KeyStroke.getKeyStroke( KeyEvent.VK_R, KeyEvent.CTRL_MASK + KeyEvent.ALT_MASK, false); private final Action uirobot = new AbstractAction() { public void actionPerformed(ActionEvent e) { setEnabled(false); // stop any other events from interfering JOptionPane.showMessageDialog(GlobalHotkeyManagerTest.this, "UIRobot Hotkey was pressed"); setEnabled(true); } }; public GlobalHotkeyManagerTest() { super("Global Hotkey Manager Test"); setSize(500,400); getContentPane().setLayout(new FlowLayout()); getContentPane().add(new JButton("Button 1")); getContentPane().add(new JTextField(20)); getContentPane().add(new JButton("Button 2")); GlobalHotkeyManager hotkeyManager = GlobalHotkeyManager.getInstance(); hotkeyManager.getInputMap().put(uirobotHotkey, UIROBOT_KEY); hotkeyManager.getActionMap().put(UIROBOT_KEY, uirobot); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // JDK 1.3 setVisible(true); } public static void main(String[] args) { new GlobalHotkeyManagerTest(); } }We basically match a KeyStroke (CTRL+ALT+R) with an Action. Since the action can be invoked from anywhere, we must remember to switch it off while we are handling it, otherwise it could be invoked again by mistake. Try out what happens when you don't disable the action and press the hotkey twice.
This is one of the most interesting things I've discovered in Swing and is so extremely useful that I do not understand why it is not more widely publicised. Please let me know if you've done something similar in your coding. It seems that SUN are quite good at adding new features or improving code without bothering to tell anyone, or at least not me! ;-) At more than 500'000 lines of code in the JDK 1.3, it becomes tiresome to read through it all each time a new release comes out.
By now, you have hopefully seen the value of understanding OO Design Patterns if you want to become good at Java. I have found that Java lends itself to good OO design, certainly more than C++.
I want to thank all of you who took the time to read and respond to my newsletters, your feedback is what really makes this worthwhile.
created by Dr. Heinz M. Kabutz
0 komentar:
Posting Komentar