001package jmri.jmrix.jinput; 002 003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 004 005import java.beans.PropertyChangeListener; 006import java.beans.PropertyChangeSupport; 007import java.util.Arrays; 008 009import javax.swing.SwingUtilities; 010import javax.swing.tree.DefaultMutableTreeNode; 011import javax.swing.tree.DefaultTreeModel; 012 013import jmri.util.SystemType; 014 015import net.java.games.input.Component; 016import net.java.games.input.Controller; 017import net.java.games.input.ControllerEnvironment; 018import net.java.games.input.Event; 019import net.java.games.input.EventQueue; 020 021import org.slf4j.Logger; 022import org.slf4j.LoggerFactory; 023 024/** 025 * TreeModel represents the USB controllers and components 026 * <p> 027 * Accessed via the instance() member, as we expect to have only one of these 028 * models talking to the USB subsystem. 029 * <p> 030 * The tree has three levels below the uninteresting root: 031 * <ol> 032 * <li>USB controller 033 * <li>Components (input, axis) 034 * </ol> 035 * <p> 036 * jinput requires that there be only one of these for a given USB system in a 037 * given JVM so we use a pseudo-singlet "instance" approach 038 * <p> 039 * Class is final because it starts a survey thread, which runs while 040 * constructor is still active. 041 * 042 * @author Bob Jacobsen Copyright 2008, 2010 043 */ 044@SuppressFBWarnings(value = "SING_SINGLETON_IMPLEMENTS_SERIALIZABLE", 045 justification = "DefaultTreeModel implements Serializable") 046public final class TreeModel extends DefaultTreeModel { 047 048 private TreeModel() { 049 050 super(new DefaultMutableTreeNode("Root")); 051 dRoot = (DefaultMutableTreeNode) getRoot(); // this is used because we can't store the DMTN we just made during the super() call 052 053 // load initial USB objects 054 boolean pass = loadSystem(); 055 if (!pass) { 056 log.warn("loading of HID System failed"); 057 } 058 059 // If you don't call loadSystem, the following line was 060 // needed to get the display to start 061 // insertNodeInto(new UsbNode("System", null, null), dRoot, 0); 062 // start the USB gathering 063 runner = new Runner(); 064 runner.setName("jinput.TreeModel loader"); 065 runner.start(); 066 } 067 068 Runner runner; 069 070 /** 071 * Add a node to the tree if it doesn't already exist 072 * 073 * @param pChild Node to possibly be inserted; relies on equals() to avoid 074 * duplicates 075 * @param pParent Node for the parent of the resource to be scanned, e.g. 076 * where in the tree to insert it. 077 * @return node, regardless of whether needed or not 078 */ 079 private DefaultMutableTreeNode insertNode(DefaultMutableTreeNode pChild, DefaultMutableTreeNode pParent) { 080 // if already exists, just return it 081 int index; 082 index = getIndexOfChild(pParent, pChild); 083 if (index >= 0) { 084 return (DefaultMutableTreeNode) getChild(pParent, index); 085 } 086 // represent this one 087 index = pParent.getChildCount(); 088 try { 089 insertNodeInto(pChild, pParent, index); 090 } catch (IllegalArgumentException e) { 091 log.error("insertNode({}, {})", pChild, pParent, e); 092 } 093 return pChild; 094 } 095 096 DefaultMutableTreeNode dRoot; 097 098 /** 099 * Provide access to the model. There's only one, because access to the USB 100 * subsystem is required. 101 * 102 * @return the default instance of the TreeModel; creating it if necessary 103 */ 104 static public TreeModel instance() { 105 if (instanceValue == null) { 106 instanceValue = new TreeModel(); 107 } 108 return instanceValue; 109 } 110 111 // intended for test routines only 112 public void terminateThreads() throws InterruptedException { 113 if (runner == null) { 114 return; 115 } 116 runner.interrupt(); 117 runner.join(); 118 } 119 120 static private TreeModel instanceValue = null; 121 122 class Runner extends Thread { 123 124 /** 125 * Continually poll for events. Report any found. 126 */ 127 @Override 128 public void run() { 129 while (true) { 130 Controller[] controllers = ControllerEnvironment.getDefaultEnvironment().getControllers(); 131 if (controllers.length == 0) { 132 try { 133 Thread.sleep(1000); 134 } catch (InterruptedException e) { 135 Thread.currentThread().interrupt(); // retain if needed later 136 return; // interrupt kills the thread 137 } 138 continue; 139 } 140 141 for (int i = 0; i < controllers.length; i++) { 142 controllers[i].poll(); 143 144 // Now we get hold of the event queue for this device. 145 EventQueue queue = controllers[i].getEventQueue(); 146 147 // Create an event object to pass down to get populated with the information. 148 // The underlying system may not hold the data in a JInput friendly way, 149 // so it only gets converted when asked for. 150 Event event = new Event(); 151 152 // Now we read from the queue until it's empty. 153 // The 3 main things from the event are a time stamp 154 // (it's in nanos, so it should be accurate, 155 // but only relative to other events. 156 // It's purpose is for knowing the order events happened in. 157 // Then we can get the component that this event relates to, and the new value. 158 while (queue.getNextEvent(event)) { 159 Component comp = event.getComponent(); 160 float value = event.getValue(); 161 162 if (log.isDebugEnabled()) { 163 StringBuffer buffer = new StringBuffer(); 164 buffer.append(controllers[i].getName()); 165 buffer.append("] Component ["); 166 // buffer.append(event.getNanos()).append(", "); 167 buffer.append(comp.getName()).append("] changed to "); 168 if (comp.isAnalog()) { 169 buffer.append(value); 170 } else { 171 if (value == 1.0f) { 172 buffer.append("On"); 173 } else { 174 buffer.append("Off"); 175 } 176 } 177 log.debug("Name [ {}", buffer); 178 } 179 180 // ensure item exits 181 new Report(controllers[i], comp, value); 182 } 183 } 184 185 try { 186 Thread.sleep(20); 187 } catch (InterruptedException e) { 188 // interrupt kills the thread 189 return; 190 } 191 } 192 } 193 } 194 195 // we build an array of USB controllers here 196 // note they might not arrive for a while 197 Controller[] ca; 198 199 public Controller[] controllers() { 200 return Arrays.copyOf(ca, ca.length); 201 } 202 203 /** 204 * Carry a single event to the Swing thread for processing 205 */ 206 class Report implements Runnable { 207 208 Controller controller; 209 Component component; 210 float value; 211 212 Report(Controller controller, Component component, float value) { 213 this.controller = controller; 214 this.component = component; 215 this.value = value; 216 217 SwingUtilities.invokeLater(this); 218 } 219 220 /** 221 * Handle report on Swing thread to ensure tree node exists and is 222 * updated 223 */ 224 @Override 225 public void run() { 226 // ensure controller node exists directly under root 227 String cname = controller.getName() + " [" + controller.getType().toString() + "]"; 228 UsbNode cNode = UsbNode.getNode(cname, controller, null); 229 try { 230 cNode = (UsbNode) insertNode(cNode, dRoot); 231 } catch (IllegalArgumentException e) { 232 log.error("insertNode({}, {})", cNode, dRoot, e); 233 } 234 // Device (component) node 235 String dname = component.getName() + " [" + component.getIdentifier().toString() + "]"; 236 UsbNode dNode = UsbNode.getNode(dname, controller, component); 237 try { 238 dNode = (UsbNode) insertNode(dNode, cNode); 239 } catch (IllegalArgumentException e) { 240 log.error("insertNode({}, {})", dNode, cNode, e); 241 } 242 243 dNode.setValue(value); 244 245 // report change to possible listeners 246 pcs.firePropertyChange("Value", dNode, Float.valueOf(value)); 247 } 248 } 249 250 /** 251 * @return true for success 252 */ 253 @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "SF_SWITCH_NO_DEFAULT", 254 justification = "This is due to a documented false-positive source") 255 boolean loadSystem() { 256 // Get a list of the controllers JInput knows about and can interact with 257 log.debug("start looking for controllers"); 258 try { 259 ca = ControllerEnvironment.getDefaultEnvironment().getControllers(); 260 log.debug("Found {} controllers", ca.length); 261 } catch (Throwable ex) { 262 log.debug("Handling Throwable", ex); 263 // this is probably ClassNotFoundException, but that's not part of the interface 264 if (ex instanceof ClassNotFoundException) { 265 switch (SystemType.getType()) { 266 case SystemType.WINDOWS : 267 log.error("Failed to find expected library", ex); 268 //$FALL-THROUGH$ 269 default: 270 log.info("Did not find an implementation of a class needed for the interface; not proceeding"); 271 log.info("This is normal, because support isn't available for {}", SystemType.getOSName()); 272 } 273 } else { 274 log.error("Encountered Throwable while getting controllers", ex); 275 } 276 277 // could not load some component(s) 278 ca = null; 279 return false; 280 } 281 282 if (controllers().length == 0) { 283 log.warn("No controllers found; tool is probably not working"); 284 jmri.util.HelpUtil.displayHelpRef("package.jmri.jmrix.jinput.treemodel.TreeFrame"); 285 return false; 286 } 287 288 for (Controller controller : controllers()) { 289 UsbNode controllerNode = null; 290 UsbNode deviceNode = null; 291 // Get this controllers components (buttons and axis) 292 Component[] components = controller.getComponents(); 293 log.info("Controller {} has {} components", controller.getName(), components.length); 294 for (Component component : components) { 295 try { 296 if (controllerNode == null) { 297 // ensure controller node exists directly under root 298 String controllerName = controller.getName() + " [" + controller.getType().toString() + "]"; 299 controllerNode = UsbNode.getNode(controllerName, controller, null); 300 controllerNode = (UsbNode) insertNode(controllerNode, dRoot); 301 } 302 // Device (component) node 303 String componentName = component.getName(); 304 String componentIdentifierString = component.getIdentifier().toString(); 305 // Skip unknown components 306 if (!componentName.equals("Unknown") && !componentIdentifierString.equals("Unknown")) { 307 String deviceName = componentName + " [" + componentIdentifierString + "]"; 308 deviceNode = UsbNode.getNode(deviceName, controller, component); 309 deviceNode = (UsbNode) insertNode(deviceNode, controllerNode); 310 deviceNode.setValue(0.0f); 311 } 312 } catch (IllegalStateException e) { 313 // node does not allow children 314 break; // skip this controller 315 } catch (IllegalArgumentException e) { 316 // ignore components that throw IllegalArgumentExceptions 317 log.error("insertNode({}, {}) Exception", deviceNode, controllerNode, e); 318 } catch (Exception e) { 319 // log all others 320 log.error("Exception", e); 321 } 322 } 323 } 324 return true; 325 } 326 327 PropertyChangeSupport pcs = new PropertyChangeSupport(this); 328 329 public synchronized void addPropertyChangeListener(PropertyChangeListener l) { 330 pcs.addPropertyChangeListener(l); 331 } 332 333 public synchronized void removePropertyChangeListener(PropertyChangeListener l) { 334 pcs.removePropertyChangeListener(l); 335 } 336 337 private final static Logger log = LoggerFactory.getLogger(TreeModel.class); 338}