001    /*
002     * NodeTypeSelector.java
003     *
004     * Developed for the "Rethinking CS101" project. See http://www.cs101.org, the
005     * CS101 homepage or email las@olin.edu.
006     *
007     * Created on January 6, 2004, 12:18 PM
008     * Please do not redistribute without obtaining permission.
009     */
010    
011    package nodenet;
012    
013    import java.awt.Color;
014    import java.awt.Insets;
015    import java.awt.Dimension;
016    import java.awt.Component;
017    import java.awt.EventQueue;
018    
019    import java.awt.event.MouseEvent;
020    import java.awt.event.ActionEvent;
021    import java.awt.event.MouseAdapter;
022    import java.awt.event.ActionListener;
023    
024    import java.beans.PropertyChangeEvent;
025    import java.beans.PropertyChangeSupport;
026    import java.beans.PropertyChangeListener;
027    
028    import java.util.Iterator;
029    import java.util.ArrayList;
030    
031    import java.lang.reflect.Field;
032    import java.lang.reflect.Modifier;
033    
034    import javax.swing.JPanel;
035    import javax.swing.JMenuItem;
036    import javax.swing.JSeparator;
037    import javax.swing.JPopupMenu;
038    import javax.swing.JToggleButton;
039    import javax.swing.SwingConstants;
040    
041    /**
042     * Implements a custom component that manages the loaded NodeBehavior classes
043     * and allows the user to select one behavior. The selected behavior is
044     * a simple bound property, and the list of loaded NodeBehavior
045     * classes is an indexed property of this class.
046     *
047     * @author  Patrick G. Heck, gus.heck@olin.edu
048     * @version $Id: NodeTypeSelector.java,v 1.12 2004/01/16 20:48:59 gus Exp $
049     */
050    public class NodeTypeSelector extends JPanel implements NodeBehaviorProvider {
051        
052        /**
053         * indecates that the array of nodeBehaviors should be re-sized by +1 and
054         * the new behavior added in the next position.
055         */
056        public static int NEXT = -99;  // using -1 might hide loop errors!
057        
058        /** Utility field used by bound properties. */
059        private java.beans.PropertyChangeSupport propertyChangeSupport =  new PropertyChangeSupport(this);
060        
061        /** Holds value of property nodeBehaviors. */
062        private Class[] nodeBehaviors = new Class[] {};
063        
064        /** Holds value of property selectedBehavior. */
065        private java.lang.Class selectedBehavior;
066        
067        /** Holds the default generator behavior */
068        private java.lang.Class generator = GeneratorNodeBehavior.class;
069        
070        /** Holds the default terminator behavior */
071        private java.lang.Class terminator = TerminatorNodeBehavior.class;
072        
073        /** holds the minimum size to display ourselves*/
074        private java.awt.Dimension minSize;
075        
076        /** Creates new form NodeTypeSelector */
077        public NodeTypeSelector() {
078            initComponents();
079            initBehaviorButtons();
080        }
081        
082        /** This method is called from within the constructor to
083         * initialize the form.
084         * WARNING: Do NOT modify this code. The content of this method is
085         * always regenerated by the Form Editor.
086         */
087        private void initComponents() {//GEN-BEGIN:initComponents
088            behaviorButtons = new javax.swing.ButtonGroup();
089            popup = new javax.swing.JPopupMenu();
090            moveUpMenuItem = new javax.swing.JMenuItem();
091            moveDownMenuItem = new javax.swing.JMenuItem();
092            buttonScrollPane = new javax.swing.JScrollPane();
093            buttonPanel = new javax.swing.JPanel();
094    
095            moveUpMenuItem.setText("Move Up");
096            moveUpMenuItem.addActionListener(new java.awt.event.ActionListener() {
097                public void actionPerformed(java.awt.event.ActionEvent evt) {
098                    moveUpMenuItemActionPerformed(evt);
099                }
100            });
101    
102            popup.add(moveUpMenuItem);
103    
104            moveDownMenuItem.setText("Move Down");
105            moveDownMenuItem.addActionListener(new java.awt.event.ActionListener() {
106                public void actionPerformed(java.awt.event.ActionEvent evt) {
107                    moveDownMenuItemActionPerformed(evt);
108                }
109            });
110    
111            popup.add(moveDownMenuItem);
112    
113            setLayout(new java.awt.GridLayout(1, 0));
114    
115            buttonScrollPane.setAlignmentX(0.0F);
116            buttonPanel.setLayout(new javax.swing.BoxLayout(buttonPanel, javax.swing.BoxLayout.Y_AXIS));
117    
118            buttonScrollPane.setViewportView(buttonPanel);
119    
120            add(buttonScrollPane);
121    
122        }//GEN-END:initComponents
123        
124        private void moveDownMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_moveDownMenuItemActionPerformed
125            // Add your handling code here:
126            Object button = ((JMenuItem)evt.getSource()).getClientProperty("button");
127            if (button instanceof JToggleButton) {
128                Class behavior = (Class)((JToggleButton)button).getClientProperty("myBehavior");
129                moveDown(behavior);
130                refreshButtons();
131            }
132        }//GEN-LAST:event_moveDownMenuItemActionPerformed
133        
134        private void moveUpMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_moveUpMenuItemActionPerformed
135            // Add your handling code here:
136            Object button = ((JMenuItem)evt.getSource()).getClientProperty("button");
137            if (button instanceof JToggleButton) {
138                Class behavior = (Class)((JToggleButton)button).getClientProperty("myBehavior");
139                moveUp(behavior);
140                refreshButtons();
141            }
142        }//GEN-LAST:event_moveUpMenuItemActionPerformed
143        
144        
145        /**
146         * Determine if a class is a behavior loaded by the user
147         */
148        
149        private boolean isUserBehavior(Class behavior) {
150            for (int i = 0; i < this.nodeBehaviors.length; i++) {
151                if (this.nodeBehaviors[i].equals(behavior)) {
152                    return true;
153                }
154            }
155            return false;
156        }
157        
158        /**
159         * Determine if the specified class is last in the list of node behaviors.
160         */
161        private boolean isLast(Class behavior) {
162            if (this.nodeBehaviors.length > 0) {
163                return this.nodeBehaviors[this.nodeBehaviors.length-1].equals(behavior);
164            } else {
165                return false;
166            }
167        }
168        
169        /**
170         * Determine if the specified class is first in the list of node behaviors.
171         */
172        private boolean isFirst(Class behavior) {
173            if (this.nodeBehaviors.length > 0) {
174                return this.nodeBehaviors[0].equals(behavior);
175            } else {
176                return false;
177            }
178        }
179        
180        /**
181         * Move the behavior Down one position in the list if possible.
182         */
183        
184        private void moveDown(Class behavior) {
185            if (isLast(behavior) || (this.nodeBehaviors.length == 1)) {
186                return;
187            } else {
188                for (int i = 0; i < this.nodeBehaviors.length; i++) {
189                    if (this.nodeBehaviors[i].equals(behavior)) {
190                        this.nodeBehaviors[i] = this.nodeBehaviors[i + 1];
191                        this.nodeBehaviors[i + 1] = behavior;
192                        break;
193                    }
194                }
195            }
196        }
197        
198        /**
199         * Move the behavior up one position in the list if possible.
200         */
201        private void moveUp(Class behavior) {
202            if (isFirst(behavior) || (this.nodeBehaviors.length == 1)) {
203                return;
204            } else {
205                for (int i = 0; i < this.nodeBehaviors.length; i++) {
206                    if (this.nodeBehaviors[i].equals(behavior)) {
207                        this.nodeBehaviors[i] = this.nodeBehaviors[i - 1];
208                        this.nodeBehaviors[i - 1] = behavior;
209                        break;
210                    }
211                }
212            }
213        }
214        
215        /** Adds a PropertyChangeListener to the listener list.
216         * @param l The listener to add.
217         *
218         */
219        public synchronized void addPropertyChangeListener(PropertyChangeListener l) {
220            propertyChangeSupport.addPropertyChangeListener(l);
221            PropertyChangeEvent pce;
222            pce = new PropertyChangeEvent(this,"selectedBehavior",
223            null, selectedBehavior);
224            l.propertyChange(pce);
225        }
226        
227        /** Removes a PropertyChangeListener from the listener list.
228         * @param l The listener to remove.
229         *
230         */
231        public void removePropertyChangeListener(PropertyChangeListener l) {
232            propertyChangeSupport.removePropertyChangeListener(l);
233        }
234        
235        /** Indexed getter for property nodeBehaviors.
236         * @param index Index of the property.
237         * @return Value of the property at <CODE>index</CODE>.
238         *
239         */
240        public Class getNodeBehaviors(int index) {
241            return this.nodeBehaviors[index];
242        }
243        
244        /** Getter for property nodeBehaviors.
245         * @return Value of property nodeBehaviors.
246         *
247         */
248        public Class[] getNodeBehaviors() {
249            return this.nodeBehaviors;
250        }
251        
252        /**
253         * Indexed setter for property nodeBehaviors. Passing an index of
254         * {@link NodeTypeSelector#NEXT} will automatically increase the
255         * array backing this property by one, and add the specified class at
256         * the end of the array. Note that this is backed by an array, because
257         * that keeps this a legal java bean. The alternative would be to back it
258         * with a List and convert arrays to lists in
259         * {@link #setNodeBehaviors(Class[])}.
260         *
261         * @param index Index of the property.
262         * @param nodeBehaviors New value of the property at <CODE>index</CODE>.
263         *
264         */
265        public void setNodeBehaviors(int index, Class nodeBehaviors) {
266            setNodeBehaviors(index, nodeBehaviors, true);
267        }
268        
269        private void setNodeBehaviors(int index, Class nodeBehaviors,
270        boolean refresh) {
271            
272            checkClass(nodeBehaviors);
273            int realIndex = index;
274            if (index == NEXT) {
275                Class[] temp = new Class[this.nodeBehaviors.length + 1];
276                System.arraycopy(this.nodeBehaviors, 0, temp, 0,
277                this.nodeBehaviors.length);
278                realIndex = temp.length-1;
279                this.nodeBehaviors = temp;
280            }
281            this.nodeBehaviors[realIndex] = nodeBehaviors;
282            if (refresh) {
283                refreshButtons();
284            }
285        }
286        /** Setter for property nodeBehaviors.
287         * @param nodeBehaviors New value of property nodeBehaviors.
288         *
289         */
290        public void setNodeBehaviors(Class[] nodeBehaviors) {
291            this.nodeBehaviors = nodeBehaviors;
292        }
293        
294        /** Getter for property selectedBehavior.
295         * @return Value of property selectedBehavior.
296         *
297         */
298        public java.lang.Class getSelectedBehavior() {
299            return this.selectedBehavior;
300        }
301        
302        /** Setter for property selectedBehavior.
303         * @param selectedBehavior New value of property selectedBehavior.
304         *
305         */
306        public synchronized void setSelectedBehavior(Class selectedBehavior) {
307            java.lang.Class oldSelectedBehavior = this.selectedBehavior;
308            this.selectedBehavior = selectedBehavior;
309            propertyChangeSupport.firePropertyChange("selectedBehavior",
310            oldSelectedBehavior, selectedBehavior);
311        }
312        
313        /**
314         * Create a button to represent a behavior.
315         */
316        private JToggleButton createButton(Class behavior) {
317            JToggleButton btn = null;
318            try {
319                Field[] fields = behavior.getFields();
320                for (int i = 0; i < fields.length; i++) {
321                    if ("BEHAVIOR_NAME".equals(fields[i].getName())) {
322                        btn = new JToggleButton((String)fields[i].get(behavior));
323                    }
324                }
325            } catch (SecurityException se) {
326                // just ignore...
327            } catch (IllegalAccessException iae) {
328                // just ignore...
329            }
330            
331            if (btn == null) {
332                btn = new JToggleButton(behavior.getName());
333            }
334            btn.putClientProperty("myBehavior", behavior);
335            Color color = Node.getColorFromBehavior(behavior.getName());
336            btn.setIcon(new NodeNetIcon(color, false));
337            btn.setSelectedIcon(new NodeNetIcon(color, true));
338            btn.setHorizontalAlignment(SwingConstants.LEFT);
339            btn.setMaximumSize(new Dimension(1024,30));
340            
341            btn.addActionListener(new ActionListener() {
342                public void actionPerformed(ActionEvent ae) {
343                    setSelectedBehavior((Class)
344                    ((JToggleButton)ae.getSource()).
345                    getClientProperty("myBehavior"));
346                }
347            });
348            btn.addMouseListener(new MouseAdapter() {
349                public void mousePressed(MouseEvent e) {
350                    maybeShowPopup(e);
351                }
352                
353                public void mouseReleased(MouseEvent e) {
354                    maybeShowPopup(e);
355                }
356                
357                private void maybeShowPopup(MouseEvent e) {
358                    if (e.isPopupTrigger()) {
359                        Class b = (Class)((JToggleButton)e.getSource()).
360                        getClientProperty("myBehavior");
361                        if (isFirst(b) || !isUserBehavior(b)) {
362                            NodeTypeSelector.this.moveUpMenuItem.setEnabled(false);
363                        } else {
364                            NodeTypeSelector.this.moveUpMenuItem.setEnabled(true);
365                        }
366                        if (isLast(b) || !isUserBehavior(b)) {
367                            NodeTypeSelector.this.
368                            moveDownMenuItem.setEnabled(false);
369                        } else {
370                            NodeTypeSelector.this.
371                            moveDownMenuItem.setEnabled(true);
372                        }
373                        NodeTypeSelector.this.moveDownMenuItem.
374                        putClientProperty("button", e.getSource());
375                        NodeTypeSelector.this.moveUpMenuItem.
376                        putClientProperty("button", e.getSource());
377                        NodeTypeSelector.this.popup.show(e.getComponent(),
378                        e.getX(), e.getY());
379                    }
380                }
381            });
382            
383            return btn;
384        }
385        
386        /**
387         * checks a class to see if it is a valid and working node behavior.
388         */
389        private void checkClass(Class behavior) {
390            String behaviorName = behavior.getName();
391            
392            //Check to see if it implements NodeBehavior
393            Class nb = NodeBehavior.class;
394            Class[] interfaces = behavior.getInterfaces();
395            boolean found = false;
396            for (int i=0; i<interfaces.length; i++) {
397                if (interfaces[i].equals(nb)) {
398                    found = true;
399                    break;
400                }
401            }
402            if (!found) {
403                throw new IllegalArgumentException(behaviorName +
404                " does not implement NodeBehavior.");
405            }
406            
407            // Check that it isn't abstract or an interface
408            int modifiers = behavior.getModifiers();
409            if (Modifier.isAbstract(modifiers)
410            || Modifier.isInterface(modifiers)) {
411                throw new IllegalArgumentException(behaviorName +
412                " must be a non-abstract class.");
413            }
414            
415            // Check that it can be instantiated
416            
417            try {
418                Object o = behavior.newInstance();
419            } catch (IllegalAccessException iae) {
420                iae.printStackTrace();
421                throw new IllegalArgumentException(behaviorName +
422                " must have a zero-argument constructor.");
423            } catch (InstantiationException ie) {
424                // shouldn't happen (we know it's a non-abstract class)
425                throw new Error("Error in checkClass algorithm");
426            }
427            
428        }
429        
430        /**
431         * Creates and adds a fresh set of buttons based on the contents of
432         * nodeBehaviors. This should only be called from refreshButtons.
433         */
434        private void addButtons() {
435            JToggleButton btn = createButton(generator);
436            this.buttonPanel.add(btn);
437            this.behaviorButtons.add(btn);
438            btn.doClick();
439            
440            JSeparator sep = new JSeparator(SwingConstants.HORIZONTAL);
441            sep.setMaximumSize(new Dimension(2000, 5));
442            this.buttonPanel.add(sep);
443            
444            for (int i = 0; i < nodeBehaviors.length; i++) {
445                JToggleButton userBtn = createButton(this.getNodeBehaviors(i));
446                this.buttonPanel.add(userBtn);
447                this.behaviorButtons.add(userBtn);
448            }
449            
450            sep = new JSeparator(SwingConstants.HORIZONTAL);
451            sep.setMaximumSize(new Dimension(2000, 5));
452            this.buttonPanel.add(sep);
453            
454            btn = createButton(terminator);
455            this.buttonPanel.add(btn);
456            this.behaviorButtons.add(btn);
457            //this.setSize(btn.getPreferredSize().width, this.getHeight());
458        }
459        
460        /**
461         * This should never ever be called except from refreshButtons.
462         * Don't use it. Use refreshButtons() instead. You have been warned.
463         */
464        private void doRefreshInternal() {
465            Component[] ourStuff =
466            NodeTypeSelector.this.buttonPanel.getComponents();
467            
468            for (int i = 0; i < ourStuff.length; i++) {
469                NodeTypeSelector.this.buttonPanel.remove(ourStuff[i]);
470                try {
471                    NodeTypeSelector.this.behaviorButtons.
472                    remove((JToggleButton)ourStuff[i]);
473                } catch (ClassCastException cce) {
474                    // ignore
475                }
476            }
477            NodeTypeSelector.this.addButtons();
478            NodeTypeSelector.this.invalidate();
479            NodeTypeSelector.this.validate();
480            
481        }
482        
483        /**
484         * Delete and recreate all the buttons based on the contents of
485         * nodeBehaviors. This sort of GUI manipulation needs to be done in the
486         * Event Dispatcher thread to avoid deadlock and other nasties in the GUI.
487         * (This can occur when the user's thread tries to modify the GUI, at the
488         * same time as the Event Dispatch thread, and each winds up holding a lock
489         * on something the other one needs). It is rare, but not impossible. In
490         * this case I am touching as much as 10=20% of the components in the
491         * GUI so I'm doing it the safe way.
492         */
493        private void refreshButtons() {
494            if (!EventQueue.isDispatchThread() ) {
495                try {
496                    EventQueue.invokeAndWait( new Runnable() {
497                        public void run() {
498                            //System.out.println("invoked");
499                            doRefreshInternal();
500                        }
501                    });
502                } catch (InterruptedException ie) {
503                    System.err.println("Interrupted while refreshing buttons");
504                    
505                } catch (java.lang.reflect.InvocationTargetException ite) {
506                    System.err.println("Exception while refreshing buttons:" + ite);
507                    ite.printStackTrace();
508                }
509            } else {
510                doRefreshInternal();
511            }
512        }
513        
514        /**
515         * Load a behavior class if possible. Returns null if the class is not
516         * not found.
517         */
518        public Class loadBehavior(String behaviorName, ClassLoader loader)  {
519            Class behavior = null;
520            try {
521                behavior = Class.forName(behaviorName, true, loader);
522                System.out.println(behaviorName + " loaded successfully");
523            } catch (ClassNotFoundException cnfe) {
524                System.err.println(behaviorName +
525                " not found, skipping...");
526            }
527            return behavior;
528        }
529        
530        /**
531         * Initializes the buttons with classes specified on the command line.
532         */
533        private void initBehaviorButtons() {
534            
535            if( Main.loadedBehaviors.size() == 0 ) {
536                this.setNodeBehaviors(NEXT, IntermediateNodeBehavior.class, false);
537            } else {
538                for (Iterator iter = Main.loadedBehaviors.iterator();
539                iter.hasNext();) {
540                    Class behavior = loadBehavior((String)iter.next(), this.getClass().getClassLoader());
541                    if (behavior == null) {
542                        continue;
543                    }
544                    if (iter.hasNext()) {
545                        this.setNodeBehaviors(NEXT,behavior,false);
546                    } else {
547                        this.setNodeBehaviors(NEXT,behavior,true);
548                    }
549                }
550            }
551            refreshButtons();
552            
553            //for (int i = 0; i< this.
554        }
555        
556        // I wish I knew how to configure where netbeans puts these.
557        // Variables declaration - do not modify//GEN-BEGIN:variables
558        private javax.swing.ButtonGroup behaviorButtons;
559        private javax.swing.JPanel buttonPanel;
560        private javax.swing.JScrollPane buttonScrollPane;
561        private javax.swing.JMenuItem moveDownMenuItem;
562        private javax.swing.JMenuItem moveUpMenuItem;
563        private javax.swing.JPopupMenu popup;
564        // End of variables declaration//GEN-END:variables
565        
566        
567    }