001 package com.croftsoft.apps.infant; 002 003 import java.awt.*; 004 import java.awt.event.*; 005 import java.io.*; 006 import java.text.*; 007 import java.util.*; 008 import javax.imageio.*; 009 import javax.swing.*; 010 import javax.swing.border.*; 011 import javax.swing.event.*; 012 013 import com.croftsoft.core.gui.LogPanel; 014 import com.croftsoft.core.gui.WindowLib; 015 import com.croftsoft.core.gui.event.UserInputListener; 016 import com.croftsoft.core.lang.NullArgumentException; 017 import com.croftsoft.core.lang.lifecycle.Destroyable; 018 import com.croftsoft.core.lang.lifecycle.Updatable; 019 import com.croftsoft.core.math.MathConstants; 020 import com.croftsoft.core.util.slot.Slot; 021 022 /********************************************************************* 023 * View. 024 * 025 * @version 026 * $Id: InfantView.java,v 1.38 2008/09/20 05:01:49 croft Exp $ 027 * @since 028 * 2006-01-03 029 * @author 030 * <a href="https://www.croftsoft.com/">David Wallace Croft</a> 031 *********************************************************************/ 032 033 public final class InfantView 034 implements Destroyable, Updatable 035 ////////////////////////////////////////////////////////////////////// 036 ////////////////////////////////////////////////////////////////////// 037 { 038 039 private static final long serialVersionUID = 0L; 040 041 private static final DateFormat dateFormat 042 = new SimpleDateFormat ( "yyyy-MM-dd HH:mm:ss Z" ); 043 044 // 045 046 private final InfantConfig infantConfig; 047 048 private final InfantAccessor infantAccessor; 049 050 private final Queue<InfantMessage> eventQueue; 051 052 private final Slot<InfantMessage> requestSlot; 053 054 private final JFrame controlFrame; 055 056 private final JFrame displayFrame; 057 058 private final InfantComponent infantComponent; 059 060 private final Set<JButton> buttonSet; 061 062 private final Map<String,Image> imagePathToImageMap; 063 064 private final SpinnerNumberModel 065 controllerIndexSpinnerNumberModel, 066 imageDisplayTimeSpinnerNumberModel, 067 interStimulusIntervalSpinnerNumberModel, 068 scaleSpinnerNumberModel; 069 070 ////////////////////////////////////////////////////////////////////// 071 ////////////////////////////////////////////////////////////////////// 072 073 public InfantView ( 074 final InfantConfig infantConfig, 075 final InfantAccessor infantAccessor, 076 final Queue<InfantMessage> eventQueue, 077 final Slot<InfantMessage> requestSlot ) 078 ////////////////////////////////////////////////////////////////////// 079 { 080 NullArgumentException.checkArgs ( 081 this.infantConfig = infantConfig, 082 this.infantAccessor = infantAccessor, 083 this.eventQueue = eventQueue, 084 this.requestSlot = requestSlot ); 085 086 buttonSet = new HashSet<JButton> ( ); 087 088 imageDisplayTimeSpinnerNumberModel = new SpinnerNumberModel ( 089 infantAccessor.getImageDisplayTime ( ) 090 * MathConstants.SECONDS_PER_NANOSECOND, 091 0.0, 092 Double.POSITIVE_INFINITY, 093 0.1 ); 094 095 interStimulusIntervalSpinnerNumberModel = new SpinnerNumberModel ( 096 infantAccessor.getInterStimulusInterval ( ) 097 * MathConstants.SECONDS_PER_NANOSECOND, 098 0.0, 099 Double.POSITIVE_INFINITY, 100 0.1 ); 101 102 controllerIndexSpinnerNumberModel = new SpinnerNumberModel ( 103 new Integer ( infantConfig.getControllerIndex ( ) ), 104 new Integer ( 0 ), 105 new Integer ( Integer.MAX_VALUE ), 106 new Integer ( 1 ) ); 107 108 scaleSpinnerNumberModel = new SpinnerNumberModel ( 109 infantAccessor.getScale ( ), 110 0.00001, 111 10000.0, 112 0.1 ); 113 114 this.imageDisplayTimeSpinnerNumberModel.addChangeListener ( 115 new ChangeListener ( ) 116 { 117 public void stateChanged ( final ChangeEvent changeEvent ) 118 { 119 final Object value 120 = imageDisplayTimeSpinnerNumberModel.getValue ( ); 121 122 requestSlot.offer ( 123 new InfantMessage ( 124 InfantMessage.Type.DURATION, 125 new Long ( 126 Math.round ( ( ( Double ) value ).doubleValue ( ) 127 * MathConstants.NANOSECONDS_PER_SECOND ) ) ) ); 128 } 129 } ); 130 131 this.interStimulusIntervalSpinnerNumberModel.addChangeListener ( 132 new ChangeListener ( ) 133 { 134 public void stateChanged ( final ChangeEvent changeEvent ) 135 { 136 final Object value 137 = interStimulusIntervalSpinnerNumberModel.getValue ( ); 138 139 requestSlot.offer ( 140 new InfantMessage ( 141 InfantMessage.Type.ISI, 142 new Long ( 143 Math.round ( ( ( Double ) value ).doubleValue ( ) 144 * MathConstants.NANOSECONDS_PER_SECOND ) ) ) ); 145 } 146 } ); 147 148 controllerIndexSpinnerNumberModel.addChangeListener ( 149 new ChangeListener ( ) 150 { 151 public void stateChanged ( final ChangeEvent changeEvent ) 152 { 153 final Object value 154 = controllerIndexSpinnerNumberModel.getValue ( ); 155 156 requestSlot.offer ( 157 new InfantMessage ( 158 InfantMessage.Type.SET_CONTROLLER_INDEX, 159 value ) ); 160 } 161 } ); 162 163 scaleSpinnerNumberModel.addChangeListener ( 164 new ChangeListener ( ) 165 { 166 public void stateChanged ( final ChangeEvent changeEvent ) 167 { 168 final Object value = scaleSpinnerNumberModel.getValue ( ); 169 170 requestSlot.offer ( 171 new InfantMessage ( 172 InfantMessage.Type.SET_SCALE_REQUEST, 173 value ) ); 174 } 175 } ); 176 177 controlFrame = createControlFrame ( 178 infantConfig, 179 buttonSet, 180 controllerIndexSpinnerNumberModel, 181 imageDisplayTimeSpinnerNumberModel, 182 interStimulusIntervalSpinnerNumberModel, 183 scaleSpinnerNumberModel ); 184 185 infantComponent 186 = new InfantComponent ( infantConfig, infantAccessor ); 187 188 displayFrame = createStimulusFrame ( 189 infantComponent, requestSlot ); 190 191 imagePathToImageMap = new HashMap<String,Image> ( ); 192 193 final String [ ] imagePaths = infantConfig.getImagePaths ( ); 194 195 try 196 { 197 for ( int id = 0; id < imagePaths.length; id++ ) 198 { 199 final String imagePath = imagePaths [ id ]; 200 201 final File file = new File ( imagePath ); 202 203 final Image image = ImageIO.read ( file ); 204 205 if ( image == null ) 206 { 207 infantConfig.getLog ( ).record ( 208 "no registered ImageReader for " + imagePath ); 209 } 210 else 211 { 212 imagePathToImageMap.put ( imagePath, image ); 213 214 infantConfig.getLog ( ).record ( imagePath ); 215 } 216 } 217 } 218 catch ( Exception ex ) 219 { 220 infantConfig.getLog ( ).record ( ex ); 221 } 222 } 223 224 ////////////////////////////////////////////////////////////////////// 225 // accessor methods 226 ////////////////////////////////////////////////////////////////////// 227 228 public JFrame getJFrame ( ) { return this.controlFrame; } 229 230 ////////////////////////////////////////////////////////////////////// 231 ////////////////////////////////////////////////////////////////////// 232 233 public void addUserInputListener ( 234 final UserInputListener userInputListener ) 235 ////////////////////////////////////////////////////////////////////// 236 { 237 this.displayFrame.addKeyListener ( userInputListener ); 238 239 for ( JButton button : this.buttonSet ) 240 { 241 button.addActionListener ( userInputListener ); 242 } 243 } 244 245 ////////////////////////////////////////////////////////////////////// 246 // lifecycle methods 247 ////////////////////////////////////////////////////////////////////// 248 249 public void destroy ( ) 250 ////////////////////////////////////////////////////////////////////// 251 { 252 this.displayFrame.setVisible ( false ); 253 254 this.controlFrame.setVisible ( false ); 255 256 this.displayFrame.dispose ( ); 257 258 this.controlFrame.dispose ( ); 259 } 260 261 public void update ( ) 262 ////////////////////////////////////////////////////////////////////// 263 { 264 InfantMessage modelEvent = null; 265 266 while ( ( modelEvent = this.eventQueue.poll ( ) ) != null ) 267 { 268 final InfantMessage.Type type = modelEvent.getType ( ); 269 270 switch ( type ) 271 { 272 case DURATION_CHANGED: 273 274 final Double duration 275 = new Double ( infantAccessor.getImageDisplayTime ( ) 276 * MathConstants.SECONDS_PER_NANOSECOND ); 277 278 final String durationString 279 = String.format ( "%1$1.3f", duration ); 280 281 final Double oldDuration = ( Double ) 282 imageDisplayTimeSpinnerNumberModel.getValue ( ); 283 284 final String oldDurationString 285 = String.format ( "%1$1.3f", oldDuration ); 286 287 if ( !durationString.equals ( oldDurationString ) ) 288 { 289 imageDisplayTimeSpinnerNumberModel.setValue ( duration ); 290 } 291 292 infantConfig.getLog ( ).record ( 293 createLogPrefix ( ) 294 + type.name ( ) 295 + " " 296 + durationString ); 297 298 break; 299 300 case GET_SAVE_REPORT_FILENAME: 301 302 getSaveReportFilename ( modelEvent ); 303 304 break; 305 306 case IMAGE: 307 308 final String imagePath 309 = infantAccessor.getImagePath ( ); 310 311 final Image image = imagePathToImageMap.get ( imagePath ); 312 313 infantComponent.setImage ( image ); 314 /* 315 this.infantConfig.getLog ( ).record ( 316 createLogPrefix ( ) 317 + modelEvent.name ( ) 318 + " \"" 319 + ( imagePath == null ? "" : imagePath ) 320 + "\"" ); 321 */ 322 323 // System.out.println ( imagePath ); 324 325 break; 326 327 case ISI_CHANGED: 328 329 final Double isi = new Double ( 330 infantAccessor.getInterStimulusInterval ( ) 331 * MathConstants.SECONDS_PER_NANOSECOND ); 332 333 final String isiString 334 = String.format ( "%1$1.3f", isi ); 335 336 final Double oldIsi = ( Double ) 337 interStimulusIntervalSpinnerNumberModel.getValue ( ); 338 339 final String oldIsiString 340 = String.format ( "%1$1.3f", oldIsi ); 341 342 if ( !isiString.equals ( oldIsiString ) ) 343 { 344 this.interStimulusIntervalSpinnerNumberModel.setValue ( 345 isi ); 346 } 347 348 this.infantConfig.getLog ( ).record ( 349 createLogPrefix ( ) 350 + type.name ( ) 351 + " " 352 + isiString ); 353 354 break; 355 356 case LOAD_SETUP: 357 358 loadSetup ( ); 359 360 break; 361 362 case STIMULUS_WINDOW_CLOSE: 363 364 this.displayFrame.setVisible ( false ); 365 366 break; 367 368 case STIMULUS_WINDOW_OPEN: 369 370 displayFrame.setExtendedState ( Frame.NORMAL ); 371 372 displayFrame.setVisible ( true ); 373 374 displayFrame.requestFocus ( ); 375 376 break; 377 378 default: 379 380 infantConfig.getLog ( ).record ( 381 createLogPrefix ( ) 382 + "InfantView: unknown command: " + type.name ( ) ); 383 } 384 } 385 } 386 387 ////////////////////////////////////////////////////////////////////// 388 ////////////////////////////////////////////////////////////////////// 389 390 private static JFrame createControlFrame ( 391 final InfantConfig infantConfig, 392 final Set<JButton> buttonSet, 393 final SpinnerNumberModel controllerIndexSpinnerNumberModel, 394 final SpinnerNumberModel imageDisplayTimeSpinnerNumberModel, 395 final SpinnerNumberModel interStimulusIntervalSpinnerNumberModel, 396 final SpinnerNumberModel scaleSpinnerNumberModel ) 397 ////////////////////////////////////////////////////////////////////// 398 { 399 final JFrame controlFrame = new JFrame ( 400 infantConfig.getFrameTitle ( ) ); 401 402 WindowLib.centerOnScreen ( 403 controlFrame, infantConfig.getFrameSize ( ) ); 404 405 final Container contentPane = controlFrame.getContentPane ( ); 406 407 contentPane.setLayout ( new BorderLayout ( ) ); 408 409 final LogPanel logPanel 410 = new LogPanel ( infantConfig.getLogLinesMax ( ) ); 411 412 infantConfig.setLog ( logPanel ); 413 414 contentPane.add ( 415 createLogScrollPane ( infantConfig, logPanel ), 416 BorderLayout.CENTER ); 417 418 final JPanel spinnerPanel = new JPanel ( new GridBagLayout ( ) ); 419 420 final GridBagConstraints gridBagConstraints 421 = new GridBagConstraints ( ); 422 423 gridBagConstraints.fill = GridBagConstraints.HORIZONTAL; 424 425 gridBagConstraints.insets = new Insets ( 1, 4, 1, 0 ); 426 427 addImageDisplayTimeSpinner ( 428 spinnerPanel, 429 gridBagConstraints, 430 imageDisplayTimeSpinnerNumberModel ); 431 432 addInterStimulusIntervalSpinner ( 433 spinnerPanel, 434 gridBagConstraints, 435 interStimulusIntervalSpinnerNumberModel ); 436 437 addControllerIndexSpinner ( 438 spinnerPanel, 439 gridBagConstraints, 440 controllerIndexSpinnerNumberModel ); 441 442 addScaleSpinner ( 443 spinnerPanel, 444 gridBagConstraints, 445 scaleSpinnerNumberModel ); 446 447 final JPanel buttonPanel = new JPanel ( new GridLayout ( 5, 1 ) ); 448 449 final JButton loadSetupButton = new JButton ( "Load Setup" ); 450 451 loadSetupButton.setActionCommand ( 452 InfantMessage.Type.LOAD_SETUP.name ( ) ); 453 454 buttonPanel.add ( loadSetupButton ); 455 456 final JButton displayButton = new JButton ( "Begin Experiment" ); 457 458 displayButton.setActionCommand ( 459 InfantMessage.Type.EXPERIMENT_BEGIN.name ( ) ); 460 461 buttonPanel.add ( displayButton ); 462 463 final JButton endButton = new JButton ( "End Experiment" ); 464 465 endButton.setActionCommand ( 466 InfantMessage.Type.EXPERIMENT_END.name ( ) ); 467 468 buttonPanel.add ( endButton ); 469 470 final JButton resetButton = new JButton ( "Save Data" ); 471 472 resetButton.setActionCommand ( 473 InfantMessage.Type.REQUEST_SAVE_FILENAME.name ( ) ); 474 475 buttonPanel.add ( resetButton ); 476 477 final JButton clearButton = new JButton ( "Clear Log" ); 478 479 buttonPanel.add ( clearButton ); 480 481 buttonSet.add ( displayButton ); 482 483 buttonSet.add ( endButton ); 484 485 buttonSet.add ( resetButton ); 486 487 buttonSet.add ( loadSetupButton ); 488 489 // buttonSet.add ( clearButton ); 490 491 clearButton.addActionListener ( 492 new ActionListener ( ) 493 { 494 public void actionPerformed ( final ActionEvent actionEvent ) 495 { 496 logPanel.clear ( ); 497 } 498 } ); 499 500 final JPanel westPanel = new JPanel ( ); 501 502 westPanel.setLayout ( 503 new BoxLayout ( westPanel, BoxLayout.Y_AXIS ) ); 504 505 westPanel.add ( spinnerPanel ); 506 507 westPanel.add ( buttonPanel ); 508 509 westPanel.add ( new Box.Filler ( 510 new Dimension ( 0, 0 ), 511 new Dimension ( 0, Integer.MAX_VALUE ), 512 new Dimension ( 0, Integer.MAX_VALUE ) ) ); 513 514 contentPane.add ( westPanel, BorderLayout.WEST ); 515 516 return controlFrame; 517 } 518 519 private static JFrame createStimulusFrame ( 520 final InfantComponent infantComponent, 521 final Slot<InfantMessage> requestSlot ) 522 ////////////////////////////////////////////////////////////////////// 523 { 524 final JFrame displayFrame = new JFrame ( ); 525 526 displayFrame.addWindowListener ( 527 new WindowAdapter ( ) 528 { 529 @Override 530 public void windowClosing ( final WindowEvent windowEvent ) 531 { 532 requestSlot.offer ( 533 new InfantMessage ( 534 InfantMessage.Type.STIMULUS_WINDOW_CLOSING ) ); 535 } 536 } ); 537 538 displayFrame.setUndecorated ( true ); 539 540 final Dimension screenSize 541 = Toolkit.getDefaultToolkit ( ).getScreenSize ( ); 542 543 displayFrame.setBounds ( 544 0, 0, screenSize.width, screenSize.height ); 545 546 final Container displayContentPane 547 = displayFrame.getContentPane ( ); 548 549 displayContentPane.setLayout ( new BorderLayout ( ) ); 550 551 displayContentPane.add ( infantComponent, BorderLayout.CENTER ); 552 553 return displayFrame; 554 } 555 556 private static void addControllerIndexSpinner ( 557 final JPanel spinnerPanel, 558 final GridBagConstraints gridBagConstraints, 559 final SpinnerNumberModel controllerIndexSpinnerNumberModel ) 560 ////////////////////////////////////////////////////////////////////// 561 { 562 final JSpinner controllerIndexSpinner 563 = new JSpinner ( controllerIndexSpinnerNumberModel ); 564 565 final JSpinner.NumberEditor numberEditor 566 = new JSpinner.NumberEditor ( 567 controllerIndexSpinner, "0" ); 568 569 controllerIndexSpinner.setEditor ( numberEditor ); 570 571 final JLabel jLabel = new JLabel ( "Controller" ); 572 573 jLabel.setBorder ( 574 BorderFactory.createEmptyBorder ( 2, 2, 2, 2 ) ); 575 576 gridBagConstraints.gridx = 0; 577 578 spinnerPanel.add ( jLabel, gridBagConstraints ); 579 580 gridBagConstraints.gridx = 1; 581 582 spinnerPanel.add ( controllerIndexSpinner, gridBagConstraints ); 583 } 584 585 private static void addImageDisplayTimeSpinner ( 586 final JPanel spinnerPanel, 587 final GridBagConstraints gridBagConstraints, 588 final SpinnerNumberModel imageDisplayTimeSpinnerNumberModel ) 589 ////////////////////////////////////////////////////////////////////// 590 { 591 final JSpinner imageDisplayTimeSpinner 592 = new JSpinner ( imageDisplayTimeSpinnerNumberModel ); 593 594 final JSpinner.NumberEditor numberEditor 595 = new JSpinner.NumberEditor ( 596 imageDisplayTimeSpinner, "0.0##" ); 597 598 imageDisplayTimeSpinner.setEditor ( numberEditor ); 599 600 final JLabel imageDisplayTimeLabel = new JLabel ( "Duration (s)" ); 601 602 imageDisplayTimeLabel.setBorder ( 603 BorderFactory.createEmptyBorder ( 2, 2, 2, 2 ) ); 604 605 gridBagConstraints.gridx = 0; 606 607 spinnerPanel.add ( imageDisplayTimeLabel, gridBagConstraints ); 608 609 gridBagConstraints.gridx = 1; 610 611 spinnerPanel.add ( imageDisplayTimeSpinner, gridBagConstraints ); 612 } 613 614 private static void addInterStimulusIntervalSpinner ( 615 final JPanel spinnerPanel, 616 final GridBagConstraints gridBagConstraints, 617 final SpinnerNumberModel interStimulusIntervalSpinnerNumberModel ) 618 ////////////////////////////////////////////////////////////////////// 619 { 620 final JSpinner interStimulusIntervalSpinner 621 = new JSpinner ( interStimulusIntervalSpinnerNumberModel ); 622 623 final JSpinner.NumberEditor numberEditor 624 = new JSpinner.NumberEditor ( 625 interStimulusIntervalSpinner, "0.0##" ); 626 627 interStimulusIntervalSpinner.setEditor ( numberEditor ); 628 629 final JLabel jLabel = new JLabel ( "ISI (s)" ); 630 631 jLabel.setBorder ( 632 BorderFactory.createEmptyBorder ( 2, 2, 2, 2 ) ); 633 634 gridBagConstraints.gridx = 0; 635 636 spinnerPanel.add ( jLabel, gridBagConstraints ); 637 638 gridBagConstraints.gridx = 1; 639 640 spinnerPanel.add ( 641 interStimulusIntervalSpinner, gridBagConstraints ); 642 } 643 644 private static void addScaleSpinner ( 645 final JPanel spinnerPanel, 646 final GridBagConstraints gridBagConstraints, 647 final SpinnerNumberModel spinnerNumberModel ) 648 ////////////////////////////////////////////////////////////////////// 649 { 650 final JSpinner jSpinner = new JSpinner ( spinnerNumberModel ); 651 652 final JSpinner.NumberEditor numberEditor 653 = new JSpinner.NumberEditor ( jSpinner, "0.0##" ); 654 655 jSpinner.setEditor ( numberEditor ); 656 657 final JLabel jLabel = new JLabel ( "Scale" ); 658 659 jLabel.setBorder ( 660 BorderFactory.createEmptyBorder ( 2, 2, 2, 2 ) ); 661 662 gridBagConstraints.gridx = 0; 663 664 spinnerPanel.add ( jLabel, gridBagConstraints ); 665 666 gridBagConstraints.gridx = 1; 667 668 spinnerPanel.add ( jSpinner, gridBagConstraints ); 669 } 670 671 private static String createLogPrefix ( ) 672 ////////////////////////////////////////////////////////////////////// 673 { 674 return dateFormat.format ( new Date ( ) ) + ": "; 675 } 676 677 private static JScrollPane createLogScrollPane ( 678 final InfantConfig infantConfig, 679 final LogPanel logPanel ) 680 ////////////////////////////////////////////////////////////////////// 681 { 682 logPanel.record ( infantConfig.getInfo ( ) ); 683 684 final JScrollPane jScrollPane = new JScrollPane ( logPanel ); 685 686 jScrollPane.setBorder ( 687 BorderFactory.createEtchedBorder ( EtchedBorder.RAISED ) ); 688 689 return jScrollPane; 690 } 691 692 private void loadSetup ( ) 693 ////////////////////////////////////////////////////////////////////// 694 { 695 final JFileChooser jFileChooser 696 = new JFileChooser ( InfantConfig.getInfantSetupDirPath ( ) ); 697 698 final int returnVal = jFileChooser.showOpenDialog ( controlFrame ); 699 700 if ( returnVal != JFileChooser.APPROVE_OPTION ) 701 { 702 return; 703 } 704 705 final File file = jFileChooser.getSelectedFile ( ); 706 707 if ( file != null ) 708 { 709 requestSlot.offer ( 710 new InfantMessage ( 711 InfantMessage.Type.LOAD_SETUP, 712 file ) ); 713 } 714 } 715 716 private void getSaveReportFilename ( 717 final InfantMessage infantMessage ) 718 ////////////////////////////////////////////////////////////////////// 719 { 720 final InfantData infantData 721 = ( InfantData ) infantMessage.getValue ( ); 722 723 final JFileChooser jFileChooser = new JFileChooser ( 724 InfantConfig.getInfantReportDirPath ( ) ); 725 726 final int returnVal 727 = jFileChooser.showSaveDialog ( controlFrame ); 728 729 if ( returnVal != JFileChooser.APPROVE_OPTION ) 730 { 731 return; 732 } 733 734 final File file = jFileChooser.getSelectedFile ( ); 735 736 if ( file != null ) 737 { 738 requestSlot.offer ( 739 new InfantMessage ( 740 InfantMessage.Type.SAVE_DATA, 741 new Object [ ] { file, infantData } ) ); 742 } 743 } 744 745 ////////////////////////////////////////////////////////////////////// 746 ////////////////////////////////////////////////////////////////////// 747 }