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