1 /* Copyright (C) 2005-2011 Fabio Riccardi */
2 
3 package com.lightcrafts.ui.toolkit.journal;
4 
5 import com.lightcrafts.utils.xml.XmlDocument;
6 import com.lightcrafts.utils.xml.XMLException;
7 import com.lightcrafts.utils.xml.XmlNode;
8 
9 import javax.swing.*;
10 import java.awt.*;
11 import java.awt.event.*;
12 import java.io.IOException;
13 import java.io.InputStream;
14 import java.io.OutputStream;
15 import java.util.LinkedList;
16 import java.util.Iterator;
17 
18 /**
19  * This class accumulates InputEvents on a single JFrame in a way that their
20  * complete history can be serialized and replayed.
21  * <p>
22  * It works by tapping into the AWT event queue.  Therefore it is only
23  * accessible through a singleton instance.  It also assumes that all events
24  * occur in a single frame.  It doesn't keep track of window focus at all.
25  * <p>
26  * To activate the tap, which introduces a small amount of overhead on all
27  * event dispatch, call start().  Then, to start accumulating history, press
28  * "Scroll Lock".  All mouse and keyboard activity (other than "Scroll Lock")
29  * will then accumulate in the history until you press "Scroll Lock" again.
30  * To deactivate the tap, removing the dispatch overhead and disabling
31  * accumulation, call stop().
32  * <p>
33  * To replay the history, call replay().  To stream the history somewhere,
34  * call write().  To reload a saved history, call read().
35  */
36 public class InputEventJournal implements AWTEventListener {
37 
38     /**
39      * Since the AWT event queue is a singleton, so is this class.
40      */
41     public static InputEventJournal Instance = new InputEventJournal();
42 
43     // The default key to start and stop event capture is "\".
44     private final static int DefaultTapControlKeyCode = KeyEvent.VK_BACK_SLASH;
45 
46     private TapQueueControl tapControl;
47     private XmlDocument doc;
48 
49     private LinkedList listeners;   // JournalListeners
50     int eventCount;
51 
InputEventJournal()52     private InputEventJournal() {
53         doc = new XmlDocument("InputEventJournal");
54         listeners = new LinkedList();
55     }
56 
57     /**
58      * Engage the InputEvent journalling machinery.  This means pushing a
59      * custom EventQueue that can siphon off InputEvents, imposing a small
60      * amount of overhead on event dispatch.
61      */
start()62     public void start() {
63         tapControl = new TapQueueControl(this, DefaultTapControlKeyCode);
64     }
65 
66     /**
67      * Test whether the InputEvent journalling machinery is engaged.  Returns
68      * true after start() and before stop().
69      */
isJournaling()70     public boolean isJournaling() {
71         return tapControl != null;
72     }
73 
74     /**
75      * Disengage the InputEvent journalling machinery.  This stops all event
76      * logging.
77      */
stop()78     public void stop() {
79         tapControl.dispose();
80         tapControl = null;
81     }
82 
83     /**
84      * Write out all recorded InputEvents to the given stream, in such a way
85      * that they may be retrieved later in read().
86      */
write(OutputStream out)87     public void write(OutputStream out) throws IOException {
88         doc.write(out);
89     }
90 
91     /**
92      * Reinitialize this journal from a saved stream, as generated by write().
93      */
read(InputStream in)94     public void read(InputStream in) throws IOException, XMLException {
95         doc = new XmlDocument(in);
96         // Validate the XML:
97         XmlNode root = doc.getRoot();
98         XmlNode[] children = root.getChildren();
99         JFrame frame = new JFrame();
100         for (int n=0; n<children.length; n++) {
101             createEvent(frame, children[n]);
102         }
103     }
104 
105     /**
106      * Reset this journal, forgetting all logged InputEvents.
107      */
clear()108     public void clear() {
109         doc = new XmlDocument("InputEventJournal");
110         eventCount = 0;
111     }
112 
replay(final JFrame frame)113     public void replay(final JFrame frame) {
114         Runnable runnable = new Runnable() {
115             public void run() {
116                 EventQueue queue =
117                     Toolkit.getDefaultToolkit().getSystemEventQueue();
118                 XmlNode[] nodes = doc.getRoot().getChildren();
119                 if (nodes.length == 0) {
120                     return;
121                 }
122                 notifyJournalStarted(true);
123                 eventCount = 0;
124                 try {
125                     int index = 0;
126                     InputEvent event = createEvent(frame, nodes[index++]);
127                     long when = event.getWhen();
128                     do {
129                         queue.postEvent(event);
130                         notifyJournalEvent(true);
131                         if (nodes.length > index) {
132                             event = createEvent(frame, nodes[index++]);
133                             Thread.sleep(event.getWhen() - when);
134                             when = event.getWhen();
135                         }
136                     } while (nodes.length > index);
137                 }
138                 catch (XMLException e) {
139                     System.err.println(
140                         "Error decoding event history: " + e.getMessage()
141                     );
142                 }
143                 catch (InterruptedException e) {
144                     System.err.println("History replay interrupted");
145                 }
146                 notifyJournalEnded(true);
147             }
148         };
149         Thread thread = new Thread(runnable, "InputEvent Replay");
150         thread.start();
151     }
152 
addJournalListener(JournalListener listener)153     void addJournalListener(JournalListener listener) {
154         listeners.add(listener);
155     }
156 
removeJournalListener(JournalListener listener)157     void removeJournalListener(JournalListener listener) {
158         listeners.remove(listener);
159     }
160 
notifyJournalStarted(boolean replay)161     private void notifyJournalStarted(boolean replay) {
162         for (Iterator i=listeners.iterator(); i.hasNext(); ) {
163             JournalListener listener = (JournalListener) i.next();
164             listener.journalStarted(replay);
165         }
166     }
167 
notifyJournalEvent(boolean replay)168     private void notifyJournalEvent(boolean replay) {
169         for (Iterator i=listeners.iterator(); i.hasNext(); ) {
170             JournalListener listener = (JournalListener) i.next();
171             listener.journalEvent(++eventCount, replay);
172         }
173     }
174 
notifyJournalEnded(boolean replay)175     private void notifyJournalEnded(boolean replay) {
176         for (Iterator i=listeners.iterator(); i.hasNext(); ) {
177             JournalListener listener = (JournalListener) i.next();
178             listener.journalEnded(replay);
179         }
180     }
181 
eventDispatched(AWTEvent event)182     public void eventDispatched(AWTEvent event) {
183         if (event == TapQueueControl.TapStartEvent) {
184             notifyJournalStarted(false);
185         }
186         else if (event == TapQueueControl.TapEndEvent) {
187             notifyJournalEnded(false);
188         }
189         else {
190             addEvent((InputEvent) event);
191             notifyJournalEvent(false);
192         }
193     }
194 
addEvent(InputEvent e)195     private void addEvent(InputEvent e) {
196         if (e instanceof MouseWheelEvent) {
197             addEvent((MouseWheelEvent) e);
198         }
199         else if (e instanceof KeyEvent) {
200             addEvent((KeyEvent) e);
201         }
202         else if (e instanceof MouseEvent) {
203             addEvent((MouseEvent) e);
204         }
205         else {
206             // Can't think of another kind of InputEvent.
207             assert false;
208         }
209     }
210 
addEvent(MouseEvent e)211     private void addEvent(MouseEvent e) {
212         XmlNode node = doc.getRoot().addChild("MouseEvent");
213         node.setAttribute("id", toString(e.getID()));
214         node.setAttribute("when", toString(e.getWhen()));
215         node.setAttribute("modifiers", toString(e.getModifiers()));
216         node.setAttribute("x", toString(e.getX()));
217         node.setAttribute("y", toString(e.getY()));
218         node.setAttribute("clickCount", toString(e.getClickCount()));
219         node.setAttribute("popup", toString(e.isPopupTrigger()));
220         node.setAttribute("button", toString(e.getButton()));
221     }
222 
addEvent(MouseWheelEvent e)223     private void addEvent(MouseWheelEvent e) {
224         XmlNode node = doc.getRoot().addChild("MouseWheelEvent");
225         node.setAttribute("id", toString(e.getID()));
226         node.setAttribute("when", toString(e.getWhen()));
227         node.setAttribute("modifiers", toString(e.getModifiers()));
228         node.setAttribute("x", toString(e.getX()));
229         node.setAttribute("y", toString(e.getY()));
230         node.setAttribute("clickCount", toString(e.getClickCount()));
231         node.setAttribute("popup", toString(e.isPopupTrigger()));
232         node.setAttribute("type", toString(e.getScrollType()));
233         node.setAttribute("amount", toString(e.getScrollAmount()));
234         node.setAttribute("rotation", toString(e.getWheelRotation()));
235     }
236 
addEvent(KeyEvent e)237     private void addEvent(KeyEvent e) {
238         XmlNode node = doc.getRoot().addChild("KeyEvent");
239         node.setAttribute("id", toString(e.getID()));
240         node.setAttribute("when", toString(e.getWhen()));
241         node.setAttribute("modifiers", toString(e.getModifiers()));
242         node.setAttribute("keyCode", toString(e.getKeyCode()));
243         node.setAttribute("keyChar", toString(e.getKeyChar()));
244         node.setAttribute("keyLoc", toString(e.getKeyLocation()));
245     }
246 
createEvent(JFrame frame, XmlNode node)247     private InputEvent createEvent(JFrame frame, XmlNode node)
248         throws XMLException
249     {
250         String name = node.getName();
251         if (name.equals("MouseEvent")) {
252             return createMouseEvent(frame, node);
253         }
254         else if (name.equals("MouseWheelEvent")) {
255             return createMouseWheelEvent(frame, node);
256         }
257         else if (name.equals("KeyEvent")) {
258             return createKeyEvent(frame, node);
259         }
260         else {
261             // Can't think of another kind of InputEvent.
262             assert false;
263             return null;
264         }
265     }
266 
createMouseEvent(JFrame frame, XmlNode node)267     private InputEvent createMouseEvent(JFrame frame, XmlNode node)
268         throws XMLException
269     {
270         int id = parseInt(node.getAttribute("id"));
271         long when = parseLong(node.getAttribute("when"));
272         int modifiers = parseInt(node.getAttribute("modifiers"));
273         int x = parseInt(node.getAttribute("x"));
274         int y = parseInt(node.getAttribute("y"));
275         int clickCount = parseInt(node.getAttribute("clickCount"));
276         boolean popup = parseBoolean(node.getAttribute("popup"));
277         int button = parseInt(node.getAttribute("button"));
278 
279         MouseEvent e = new MouseEvent(
280             frame, id, when, modifiers, x, y, clickCount, popup, button
281         );
282         return e;
283     }
284 
createMouseWheelEvent(JFrame frame, XmlNode node)285     private InputEvent createMouseWheelEvent(JFrame frame, XmlNode node)
286         throws XMLException
287     {
288         int id = parseInt(node.getAttribute("id"));
289         long when = parseLong(node.getAttribute("when"));
290         int modifiers = parseInt(node.getAttribute("modifiers"));
291         int x = parseInt(node.getAttribute("x"));
292         int y = parseInt(node.getAttribute("y"));
293         int clickCount = parseInt(node.getAttribute("clickCount"));
294         boolean popup = parseBoolean(node.getAttribute("popup"));
295         int type = parseInt(node.getAttribute("type"));
296         int amount = parseInt(node.getAttribute("amount"));
297         int rotation = parseInt(node.getAttribute("rotation"));
298 
299         MouseWheelEvent e = new MouseWheelEvent(
300             frame, id, when, modifiers, x, y, clickCount, popup, type,
301             amount, rotation
302         );
303         return e;
304     }
305 
createKeyEvent(JFrame frame, XmlNode node)306     private InputEvent createKeyEvent(JFrame frame, XmlNode node)
307         throws XMLException
308     {
309         int id = parseInt(node.getAttribute("id"));
310         long when = parseLong(node.getAttribute("when"));
311         int modifiers = parseInt(node.getAttribute("modifiers"));
312         int keyCode = parseInt(node.getAttribute("keyCode"));
313         char keyChar = parseChar(node.getAttribute("keyChar"));
314         int keyLoc = parseInt(node.getAttribute("keyLoc"));
315 
316         KeyEvent e = new KeyEvent(
317             frame, id, when, modifiers, keyCode, keyChar, keyLoc
318         );
319         return e;
320     }
321 
toString(char c)322     private static String toString(char c) {
323         return toString((int) c);
324     }
325 
toString(int i)326     private static String toString(int i) {
327         return Integer.toString(i);
328     }
329 
toString(long l)330     private static String toString(long l) {
331         return Long.toString(l);
332     }
333 
toString(boolean b)334     private static String toString(boolean b) {
335         return Boolean.toString(b);
336     }
337 
parseChar(String s)338     private static char parseChar(String s) {
339         return (char) parseInt(s);
340     }
341 
parseInt(String s)342     private static int parseInt(String s) {
343         return Integer.parseInt(s);
344     }
345 
parseLong(String s)346     private static long parseLong(String s) {
347         return Long.parseLong(s);
348     }
349 
parseBoolean(String s)350     private static boolean parseBoolean(String s) {
351         return Boolean.valueOf(s).booleanValue();
352     }
353 }
354