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