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}