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}