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 }