LISTSERV mailing list manager LISTSERV 16.5

Help for HPS-SVN Archives


HPS-SVN Archives

HPS-SVN Archives


HPS-SVN@LISTSERV.SLAC.STANFORD.EDU


View:

Message:

[

First

|

Previous

|

Next

|

Last

]

By Topic:

[

First

|

Previous

|

Next

|

Last

]

By Author:

[

First

|

Previous

|

Next

|

Last

]

Font:

Proportional Font

LISTSERV Archives

LISTSERV Archives

HPS-SVN Home

HPS-SVN Home

HPS-SVN  November 2014

HPS-SVN November 2014

Subject:

r1427 - in /java/trunk/ecal-event-display/src/main/java/org/hps/monitoring/ecal/eventdisplay: exec/ io/ ui/ util/

From:

[log in to unmask]

Reply-To:

Notification of commits to the hps svn repository <[log in to unmask]>

Date:

Tue, 4 Nov 2014 18:19:35 -0000

Content-Type:

text/plain

Parts/Attachments:

Parts/Attachments

text/plain (4217 lines)

Author: mccaky
Date: Tue Nov  4 10:19:31 2014
New Revision: 1427

Log:
More event display updates.

Added:
    java/trunk/ecal-event-display/src/main/java/org/hps/monitoring/ecal/eventdisplay/io/LCIOManager.java   (with props)
    java/trunk/ecal-event-display/src/main/java/org/hps/monitoring/ecal/eventdisplay/util/SettingsEvent.java   (with props)
    java/trunk/ecal-event-display/src/main/java/org/hps/monitoring/ecal/eventdisplay/util/SettingsListener.java   (with props)
Modified:
    java/trunk/ecal-event-display/src/main/java/org/hps/monitoring/ecal/eventdisplay/exec/Main.java
    java/trunk/ecal-event-display/src/main/java/org/hps/monitoring/ecal/eventdisplay/io/AdvancedReader.java
    java/trunk/ecal-event-display/src/main/java/org/hps/monitoring/ecal/eventdisplay/io/TextManager.java
    java/trunk/ecal-event-display/src/main/java/org/hps/monitoring/ecal/eventdisplay/ui/ActiveViewer.java
    java/trunk/ecal-event-display/src/main/java/org/hps/monitoring/ecal/eventdisplay/ui/CalorimeterPanel.java
    java/trunk/ecal-event-display/src/main/java/org/hps/monitoring/ecal/eventdisplay/ui/CrystalFilterPanel.java
    java/trunk/ecal-event-display/src/main/java/org/hps/monitoring/ecal/eventdisplay/ui/DataFileViewer.java
    java/trunk/ecal-event-display/src/main/java/org/hps/monitoring/ecal/eventdisplay/ui/FileViewer.java
    java/trunk/ecal-event-display/src/main/java/org/hps/monitoring/ecal/eventdisplay/ui/OccupancyViewer.java
    java/trunk/ecal-event-display/src/main/java/org/hps/monitoring/ecal/eventdisplay/ui/Viewer.java

Modified: java/trunk/ecal-event-display/src/main/java/org/hps/monitoring/ecal/eventdisplay/exec/Main.java
 =============================================================================
--- java/trunk/ecal-event-display/src/main/java/org/hps/monitoring/ecal/eventdisplay/exec/Main.java	(original)
+++ java/trunk/ecal-event-display/src/main/java/org/hps/monitoring/ecal/eventdisplay/exec/Main.java	Tue Nov  4 10:19:31 2014
@@ -1,72 +1,59 @@
 package org.hps.monitoring.ecal.eventdisplay.exec;
-
-import org.hps.monitoring.ecal.eventdisplay.io.TextManager;
 
 import java.awt.GraphicsDevice;
 import java.awt.GraphicsEnvironment;
+import java.io.File;
 import java.io.IOException;
 
-import org.hps.monitoring.ecal.eventdisplay.ui.ActiveViewer;
-import org.hps.monitoring.ecal.eventdisplay.ui.ClusterViewer;
 import org.hps.monitoring.ecal.eventdisplay.ui.FileViewer;
-import org.hps.monitoring.ecal.eventdisplay.ui.OccupancyViewer;
+import org.hps.monitoring.ecal.eventdisplay.ui.DataFileViewer;
 
 /**
- * The class <code>Main</code> can be used to create an event display that
- * reads from file. By default it reads from "cluster-hit.txt" at the class
- * path root. This can be changed by altering the line<br/>
- * <code>window.setDataSource("cluster-hit.txt")</code><br/>
- **/
+ * The class <code>Main</code> can be used to create an event display
+ * that reads from file. The input file can be set with the "-i" command,
+ * though any file can be loaded from the file menu after initialization.
+ * Calorimeter wiring information can be loaded with the"-w" command.
+ */
 public class Main {
-	
     public static void main(String[] args) throws IOException {
-    	// If "-h" was given as a command line argument, print the
-    	// help text.
-    	for(String s : args) {
-    		if(s.compareTo("-h") == 0 || s.compareTo("--help") == 0) {
-    			System.out.println("HPS Event Display");
-    			System.out.println("Options:");
-    			System.out.printf("%-4s%-12s%s%n", "-h", "--help", "Display help text.");
-    			System.out.printf("%-4s%-12s%s%n", "-i", "--input", "Defines the input file.");
-    			System.out.printf("%-4s%-12s%s%n", "-t", "--type", "Select the type of display.");
-    			System.out.printf("%-4s%-12s%s%n", "", "", "Allowed types: Event, Cluster, Occupancy");
-    			System.exit(0);
-    		}
-    	}
-    	
-    	// If -i or --input was given, set the input file.
-    	String filepath = "raw-hits.txt";
-    	for(int i = 0; i < args.length; i++) {
-    		if(args[i].compareTo("-i") == 0 || args[i].compareTo("--input") == 0) {
-    			if(args.length <= i + 1) { System.out.println("Error: Expected additional argument."); }
-    			else { filepath = args[i + 1]; }
-    		}
-    	}
-    	
-        // Define the viewer. By default, we employ a file viewer.
-        TextManager dataManager = new TextManager(filepath);
-        ActiveViewer window = new FileViewer(dataManager);
+        // If "-h" was given as a command line argument, print the
+        // help text.
+        for(String s : args) {
+            if(s.compareTo("-h") == 0 || s.compareTo("--help") == 0) {
+                System.out.println("HPS Event Display");
+                System.out.println("Options:");
+                System.out.printf("%-4s%-12s%s%n", "-h", "--help", "Display help text.");
+                System.out.printf("%-4s%-12s%s%n", "-i", "--input", "Defines the input file.");
+                System.out.printf("%-4s%-12s%s%n", "-w", "--wiring", "Defines the wiring data file.");
+                System.exit(0);
+            }
+        }
         
-        // Command line argument "-t" allows a different type to be declared.
-        if(args.length >= 2 && (args[0].compareTo("-t") == 0 || args[0].compareTo("--type") == 0)) {
-        	// If an occupancy viewer has been specified...
-        	if(args[1].compareToIgnoreCase("Occupancy") == 0) {
-                window = new OccupancyViewer(dataManager);
-        	}
-        	// If an event viewer has been specified...
-        	else if(args[1].compareToIgnoreCase("Event") == 0) {
-                window = new FileViewer(dataManager);
-        	}
-        	// If a cluster viewer has been specified...
-        	else if(args[1].compareToIgnoreCase("Cluster") == 0) {
-                window = new ClusterViewer(dataManager, 2);
-        	}
-        	// Otherwise, it is an invalid type.
-        	else {
-        		System.out.println("Display type \"" + args[1] + "\" not recognized.");
-        		System.exit(0);
-        	}
+        // If -i or --input was given, set the input file.
+        File dataSource = null;
+        String filepath = null;
+        for(int i = 0; i < args.length; i++) {
+            if(args[i].compareTo("-i") == 0 || args[i].compareTo("--input") == 0) {
+                if(args.length <= i + 1) { System.out.println("Error: Expected additional argument."); }
+                else { filepath = args[i + 1]; }
+            }
         }
+        if(filepath != null) { dataSource = new File(filepath); }
+        
+        // If -w or --wiring was given, set the input file.
+        String wiringPath = null;
+        for(int i = 0; i < args.length; i++) {
+            if(args[i].compareTo("-w") == 0 || args[i].compareTo("--witing") == 0) {
+                if(args.length <= i + 1) { System.out.println("Error: Expected additional argument."); }
+                else { wiringPath = args[i + 1]; }
+            }
+        }
+        if(filepath != null) { dataSource = new File(filepath); }
+        
+        // Define the viewer.
+        FileViewer window;
+        if(wiringPath != null) { window = new DataFileViewer(dataSource, wiringPath); }
+        else { window = new FileViewer(dataSource); }
         
         // Get screen size of primary monitor
         GraphicsDevice gd = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();
@@ -77,8 +64,5 @@
         window.setLocation((screenWidth - window.getPreferredSize().width) / 2,
                 (screenHeight - window.getPreferredSize().height) / 2);
         window.setVisible(true);
-        
-        // Start the viewer with the first event.
-        window.displayNextEvent();
     }
-}
+}

Modified: java/trunk/ecal-event-display/src/main/java/org/hps/monitoring/ecal/eventdisplay/io/AdvancedReader.java
 =============================================================================
--- java/trunk/ecal-event-display/src/main/java/org/hps/monitoring/ecal/eventdisplay/io/AdvancedReader.java	(original)
+++ java/trunk/ecal-event-display/src/main/java/org/hps/monitoring/ecal/eventdisplay/io/AdvancedReader.java	Tue Nov  4 10:19:31 2014
@@ -13,237 +13,237 @@
  * @author Kyle McCarty
  */
 public class AdvancedReader extends Reader {
-	private RandomAccessFile file;
-	private long mark = -1;
-	
-	/**
-	 * <b>AdvancedReader</b><br/><br/>
-	 * <code>public <b>AdvancedReader</b>(String filepath)</code><br/><br/>
-	 * Constructs an <code>AdvancedReader</code> from the file located
-	 * at the given file path.
-	 * @param filepath - The path to the file that is to be loaded.
-	 * @throws FileNotFoundException - Occurs when there is no file at
-	 * the indicated file path.
-	 */
-	public AdvancedReader(String filepath) throws FileNotFoundException {
-		file = new RandomAccessFile(filepath, "r");
-	}
-	
-	/**
-	 * <b>AdvancedReader</b><br/><br/>
-	 * <code>public <b>AdvancedReader</b>(File inputFile)</code><br/><br/>
-	 * Constructs an <code>AdvancedReader</code> from the indicated file.
-	 * @param inputFile - The file that is to be loaded.
-	 * @throws FileNotFoundException - Occurs when the referenced file
-	 * does not exist.
-	 */
-	public AdvancedReader(File inputFile) throws FileNotFoundException {
-		file = new RandomAccessFile(inputFile.getAbsolutePath(), "r");
-	}
-	
-	public void close() throws IOException { file.close(); }
-	
-	public void mark(int readAheadLimit) throws IOException { mark = file.getFilePointer(); }
-	
-	public boolean markSupported() { return true; }
-	
-	public int read() throws IOException { return file.read(); }
-	
-	public int read(char[] cbuf) throws IOException {
-		// Read a character from the file into each spot in the array.
-		for(int i = 0; i < cbuf.length; i++) {
-			// Get the next character.
-			int curChar = file.read();
-			
-			// If the character exists, write it to the array.
-			if(curChar != 0) { cbuf[i] = (char)curChar; }
-			
-			// Otherwise, return noting the number of filled slots.
-			else { return i; }
-		}
-		
-		// If we reach the end of the loop, all array slots are filled.
-		return cbuf.length;
-	}
-	
-	public int read(char[] cbuf, int off, int len) throws IOException {
-		// We will fill the char array until either the indicated number
-		// of slots are filled or until we run out of slots.
-		int[] max = { cbuf.length, off + len };
-		
-		// Track the number of slots written to.
-		int slotsFilled = 0;
-		
-		// Fill the array slots.
-		for(int i = off; (i < max[0] && i < max[1]); i++) {
-			// Get the next character.
-			int curChar = file.read();
-			
-			// If the character is defined, write it.
-			if(curChar != -1) {
-				cbuf[i] = (char)curChar;
-				slotsFilled++;
-			}
-			
-			// Otherwise, return noting the number of filled slots.
-			else { return slotsFilled; }
-		}
-		
-		// If we reach the end of the loop, all array slots are filled.
-		return slotsFilled;
-	}
-	
-	/**
-	 * <b>readNextLine</b><br/><br/>
-	 * <code>public String <b>readNextLine</b>()</code><br/><br/>
-	 * Reads the next line of text. A line is considered to be
-	 * terminated by any one of a line feed ('\n'), a carriage return
-	 * ('\r'), or a carriage return followed immediately by a linefeed.
-	 * @return A String containing the contents of the line, not
-	 * including any line-termination characters, or null if the end
-	 * of the stream has been reached.
-	 * @throws IOException - Occurs if there is an issue reading
-	 * the file.
-	 */
-	public String readNextLine() throws IOException {
-		// Store whether the new line we found was a carriage return
-		// or a proper newline.
-		boolean readCarriageReturn = false;
-		
-		// Add characters to a buffer until we reach either a new line
-		// or a carriage return.
-		int curChar = -1;
-		StringBuffer curLine = new StringBuffer();
-		while((curChar = file.read()) != -1) {
-			if(curChar != '\n' && curChar != '\r') { curLine.append((char)curChar); }
-			else if(curChar == '\r') {
-				readCarriageReturn = true;
-				break;
-			}
-			else { break; }
-		}
-		
-		// If we reached this point because we hit the end of the file,
-		// check to make sure that the string buffer contains something.
-		// If it doesn't, then we started at the end of the file and
-		// should return null. Otherwise, just return what is in the
-		// string buffer.
-		if(curChar == -1) {
-			if(curLine.length() == 0) { return null; }
-			else { return curLine.toString(); }
-		}
-		
-		// If we found a carriage return as the new line character, then
-		// we need to check if the next line is a newline '\n' character,
-		// since some systems us '/r/n' for a new line signifier.
-		if(readCarriageReturn) {
-			curChar = file.read();
-			if(curChar != '\n') { file.seek(file.getFilePointer() - 1); }
-		}
-		
-		// Return the buffer.
-		return curLine.toString();
-	}
-	
-	/**
-	 * <b>readPreviousLine</b><br/><br/>
-	 * <code>public String <b>readPreviousLine</b>()</code><br/><br/>
-	 * Reads the previous line of text. A line is considered to be
-	 * terminated by any one of a line feed ('\n'), a carriage return
-	 * ('\r'), or a carriage return followed immediately by a linefeed.
-	 * @return A <code>String</code> containing the contents of the
-	 * line, not including any line-termination characters, or null
-	 * if the beginning of the stream has been reached.
-	 * @throws IOException - Occurs if there is an issue reading
-	 * the file.
-	 */
-	public String readPreviousLine() throws IOException {
-		// Define variables.
-		short newlinesRead = 0;
-		boolean allowDuplicate = false;
-		int lastChar = -1;
-		long offset = file.getFilePointer();
-		int curChar;
-		
-		// If we are at the start of the file, return null.
-		if(offset == 0) { return null; }
-		
-		while(newlinesRead < 3) {	
-			// Decrement the offset.
-			offset -= 1;
-			
-			// If the offset is still within the bounds of the file,
-			// then go to it.
-			if(offset >= 0) {
-				// Get the new file position.
-				file.seek(offset);
-				
-				// Read the character there.
-				curChar = file.read();
-				
-				// If this a new line character, account for it.
-				if(curChar == '\n' || curChar == '\r') {
-					// If we have read a newline, and we then read another
-					// immediately after it, then we will accept that newline
-					// as well, since some systems use 
-					if(((lastChar == '\n' && curChar == '\r') || (lastChar == '\r' && curChar == '\n')) && allowDuplicate) {
-						allowDuplicate = false;
-					}
-					
-					// If this is the first newline character we have
-					// seen, then we have reached the end of the desired
-					// line. We need to keep going until we find the start.
-					else if(newlinesRead < 3) {
-						newlinesRead++;
-						allowDuplicate = true;
-					}
-				}
-				
-				// If a character other than a newline was read after
-				// a newline, then a second newline is always unrelated.
-				else { allowDuplicate = false; }
-				
-				// Set the current character to the last read character.
-				lastChar = curChar;
-			}
-			// If the offset has reached zero, but didn't start there,
-			// then we have reached the start of the file. We handle
-			// this case.
-			else {
-				// Set the pointer to the beginning of the file.
-				file.seek(0);
-				
-				// If we are looking for the final new line, then the
-				// start of the file counts and we should return.
-				if(newlinesRead == 2) { return readNextLine(); }
-				
-				// Otherwise, there is no previous line.
-				else { return null; }
-			}
-		}
-		
-		// If we reach this return line, we should be at the correct
-		// position to read the previous line.
-		return readNextLine();
-	}
-	
-	public void reset() throws IOException {
-		if(mark != -1) { file.seek(mark); }
-		else { throw new IOException("mark() must be called before reset()."); }
-	}
-	
-	public long skip(long n) throws IOException {
-		// Read n characters.
-		for(int i = 0; i < n; i++) {
-			// Get the skipped character.
-			int curChar = file.read();
-			
-			// If it is invalid, return.
-			if(curChar == -1) { return i; }
-		}
-		
-		// If we reached the end of the loop, the requested number of
-		// characters have been skipped.
-		return n;
-	}
-}
+    private RandomAccessFile file;
+    private long mark = -1;
+    
+    /**
+     * Constructs an <code>AdvancedReader</code> from the file located
+     * at the given file path.
+     * @param filepath - The path to the file that is to be loaded.
+     * @throws FileNotFoundException - Occurs when there is no file at
+     * the indicated file path.
+     */
+    public AdvancedReader(String filepath) throws FileNotFoundException {
+        file = new RandomAccessFile(filepath, "r");
+    }
+    
+    /**
+     * Constructs an <code>AdvancedReader</code> from the indicated file.
+     * @param inputFile - The file that is to be loaded.
+     * @throws FileNotFoundException - Occurs when the referenced file
+     * does not exist.
+     */
+    public AdvancedReader(File inputFile) throws FileNotFoundException {
+        file = new RandomAccessFile(inputFile.getAbsolutePath(), "r");
+    }
+    
+    @Override
+    public void close() throws IOException { file.close(); }
+    
+    @Override
+    public void mark(int readAheadLimit) throws IOException { mark = file.getFilePointer(); }
+    
+    @Override
+    public boolean markSupported() { return true; }
+    
+    @Override
+    public int read() throws IOException { return file.read(); }
+    
+    @Override
+    public int read(char[] cbuf) throws IOException {
+        // Read a character from the file into each spot in the array.
+        for(int i = 0; i < cbuf.length; i++) {
+            // Get the next character.
+            int curChar = file.read();
+            
+            // If the character exists, write it to the array.
+            if(curChar != 0) { cbuf[i] = (char)curChar; }
+            
+            // Otherwise, return noting the number of filled slots.
+            else { return i; }
+        }
+        
+        // If we reach the end of the loop, all array slots are filled.
+        return cbuf.length;
+    }
+    
+    @Override
+    public int read(char[] cbuf, int off, int len) throws IOException {
+        // We will fill the char array until either the indicated number
+        // of slots are filled or until we run out of slots.
+        int[] max = { cbuf.length, off + len };
+        
+        // Track the number of slots written to.
+        int slotsFilled = 0;
+        
+        // Fill the array slots.
+        for(int i = off; (i < max[0] && i < max[1]); i++) {
+            // Get the next character.
+            int curChar = file.read();
+            
+            // If the character is defined, write it.
+            if(curChar != -1) {
+                cbuf[i] = (char)curChar;
+                slotsFilled++;
+            }
+            
+            // Otherwise, return noting the number of filled slots.
+            else { return slotsFilled; }
+        }
+        
+        // If we reach the end of the loop, all array slots are filled.
+        return slotsFilled;
+    }
+    
+    /**
+     * Reads the next line of text. A line is considered to be
+     * terminated by any one of a line feed ('\n'), a carriage return
+     * ('\r'), or a carriage return followed immediately by a linefeed.
+     * @return A String containing the contents of the line, not
+     * including any line-termination characters, or null if the end
+     * of the stream has been reached.
+     * @throws IOException - Occurs if there is an issue reading
+     * the file.
+     */
+    public String readNextLine() throws IOException {
+        // Store whether the new line we found was a carriage return
+        // or a proper newline.
+        boolean readCarriageReturn = false;
+        
+        // Add characters to a buffer until we reach either a new line
+        // or a carriage return.
+        int curChar = -1;
+        StringBuffer curLine = new StringBuffer();
+        while((curChar = file.read()) != -1) {
+            if(curChar != '\n' && curChar != '\r') { curLine.append((char)curChar); }
+            else if(curChar == '\r') {
+                readCarriageReturn = true;
+                break;
+            }
+            else { break; }
+        }
+        
+        // If we reached this point because we hit the end of the file,
+        // check to make sure that the string buffer contains something.
+        // If it doesn't, then we started at the end of the file and
+        // should return null. Otherwise, just return what is in the
+        // string buffer.
+        if(curChar == -1) {
+            if(curLine.length() == 0) { return null; }
+            else { return curLine.toString(); }
+        }
+        
+        // If we found a carriage return as the new line character, then
+        // we need to check if the next line is a newline '\n' character,
+        // since some systems us '/r/n' for a new line signifier.
+        if(readCarriageReturn) {
+            curChar = file.read();
+            if(curChar != '\n') { file.seek(file.getFilePointer() - 1); }
+        }
+        
+        // Return the buffer.
+        return curLine.toString();
+    }
+    
+    /**
+     * Reads the previous line of text. A line is considered to be
+     * terminated by any one of a line feed ('\n'), a carriage return
+     * ('\r'), or a carriage return followed immediately by a linefeed.
+     * @return A <code>String</code> containing the contents of the
+     * line, not including any line-termination characters, or null
+     * if the beginning of the stream has been reached.
+     * @throws IOException - Occurs if there is an issue reading
+     * the file.
+     */
+    public String readPreviousLine() throws IOException {
+        // Define variables.
+        short newlinesRead = 0;
+        boolean allowDuplicate = false;
+        int lastChar = -1;
+        long offset = file.getFilePointer();
+        int curChar;
+        
+        // If we are at the start of the file, return null.
+        if(offset == 0) { return null; }
+        
+        while(newlinesRead < 3) {    
+            // Decrement the offset.
+            offset -= 1;
+            
+            // If the offset is still within the bounds of the file,
+            // then go to it.
+            if(offset >= 0) {
+                // Get the new file position.
+                file.seek(offset);
+                
+                // Read the character there.
+                curChar = file.read();
+                
+                // If this a new line character, account for it.
+                if(curChar == '\n' || curChar == '\r') {
+                    // If we have read a newline, and we then read another
+                    // immediately after it, then we will accept that newline
+                    // as well, since some systems use 
+                    if(((lastChar == '\n' && curChar == '\r') || (lastChar == '\r' && curChar == '\n')) && allowDuplicate) {
+                        allowDuplicate = false;
+                    }
+                    
+                    // If this is the first newline character we have
+                    // seen, then we have reached the end of the desired
+                    // line. We need to keep going until we find the start.
+                    else if(newlinesRead < 3) {
+                        newlinesRead++;
+                        allowDuplicate = true;
+                    }
+                }
+                
+                // If a character other than a newline was read after
+                // a newline, then a second newline is always unrelated.
+                else { allowDuplicate = false; }
+                
+                // Set the current character to the last read character.
+                lastChar = curChar;
+            }
+            // If the offset has reached zero, but didn't start there,
+            // then we have reached the start of the file. We handle
+            // this case.
+            else {
+                // Set the pointer to the beginning of the file.
+                file.seek(0);
+                
+                // If we are looking for the final new line, then the
+                // start of the file counts and we should return.
+                if(newlinesRead == 2) { return readNextLine(); }
+                
+                // Otherwise, there is no previous line.
+                else { return null; }
+            }
+        }
+        
+        // If we reach this return line, we should be at the correct
+        // position to read the previous line.
+        return readNextLine();
+    }
+    
+    @Override
+    public void reset() throws IOException {
+        if(mark != -1) { file.seek(mark); }
+        else { throw new IOException("mark() must be called before reset()."); }
+    }
+    
+    @Override
+    public long skip(long n) throws IOException {
+        // Read n characters.
+        for(int i = 0; i < n; i++) {
+            // Get the skipped character.
+            int curChar = file.read();
+            
+            // If it is invalid, return.
+            if(curChar == -1) { return i; }
+        }
+        
+        // If we reached the end of the loop, the requested number of
+        // characters have been skipped.
+        return n;
+    }
+}

Added: java/trunk/ecal-event-display/src/main/java/org/hps/monitoring/ecal/eventdisplay/io/LCIOManager.java
 =============================================================================
--- java/trunk/ecal-event-display/src/main/java/org/hps/monitoring/ecal/eventdisplay/io/LCIOManager.java	(added)
+++ java/trunk/ecal-event-display/src/main/java/org/hps/monitoring/ecal/eventdisplay/io/LCIOManager.java	Tue Nov  4 10:19:31 2014
@@ -0,0 +1,209 @@
+package org.hps.monitoring.ecal.eventdisplay.io;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.hps.monitoring.ecal.eventdisplay.event.Cluster;
+import org.hps.monitoring.ecal.eventdisplay.event.EcalHit;
+import org.hps.recon.ecal.HPSEcalCluster;
+import org.lcsim.event.CalorimeterHit;
+import org.lcsim.event.EventHeader;
+import org.lcsim.lcio.LCIOReader;
+
+/**
+ * Class <code>LCIOManager</code> is an implementation of <code>
+ * EventManager</code> for (S)LCIO files.<br/>
+ * <br/>
+ * <b><span style="color:red">Warning: This class is under construction
+ * and should not be used at this time!</span></b>
+ * 
+ * @author Kyle McCarty
+ */
+public class LCIOManager implements EventManager {
+    // Internal variables.
+    private LCIOReader reader;
+    private EventHeader current;
+    private int eventsRead = 0;
+    private final File sourceFile;
+    
+    // LCIO collection names.
+    private String clusterCollectionName = "EcalClusters";
+    private String hitCollectionName = "EcalHits";
+    
+    public LCIOManager(String inputFilepath) throws IOException {
+        this(new File(inputFilepath));
+    }
+    
+    public LCIOManager(File input) throws IOException {
+        // Create an LCIO reader from the file.
+        reader = new LCIOReader(input);
+        
+        // Store the source file.
+        sourceFile = input;
+        
+        // Read the first event.
+        nextEvent();
+    }
+    
+    public void setClusterCollectionName(String clusterCollectionName) {
+        this.clusterCollectionName = clusterCollectionName;
+    }
+    
+    public void setHitCollectionName(String hitCollectionName) {
+        this.hitCollectionName = hitCollectionName;
+    }
+    
+    @Override
+    public void close() throws IOException {
+        reader.close();
+    }
+    
+    @Override
+    public int getEventNumber() {
+        // If there is a currently defined event, get it's event number.
+        if(current != null) { return current.getEventNumber(); }
+        
+        // Otherwise, return -1.
+        else { return -1; }
+    }
+    
+    @Override
+    public List<Cluster> getClusters() {
+        // If the current event is undefined, return an empty list.
+        if(current == null) { return new ArrayList<Cluster>(); }
+        
+        // Otherwise, try to obtain and convert the cluster collection
+        // from the LCIO event.
+        else {
+            // Check to see if the event has a cluster collection.
+            if(current.hasCollection(HPSEcalCluster.class, clusterCollectionName)) {
+                // Get the list of LCIO clusters.
+                List<HPSEcalCluster> lcioList = current.get(HPSEcalCluster.class, clusterCollectionName);
+                
+                // Create a list to store event display clusters.
+                List<Cluster> displayList = new ArrayList<Cluster>(lcioList.size());
+                
+                // Convert the LCIO clusters to display clusters.
+                for(HPSEcalCluster lcioCluster : lcioList) {
+                    displayList.add(toPanelCluster(lcioCluster));
+                }
+                
+                // Return the converted list of clusters.
+                return displayList;
+            }
+            
+            // If it does not, return an empty list.
+            else { return new ArrayList<Cluster>(); }
+        }
+    }
+    
+    @Override
+    public List<EcalHit> getHits() {
+        System.out.println("Event is null: " + (current == null));
+        
+        // If the current event is undefined, return an empty list.
+        if(current == null) { return new ArrayList<EcalHit>(); }
+        
+        // Otherwise, try to obtain and convert the hit collection from
+        // the LCIO event.
+        else {
+            System.out.println("Check for hits...");
+            // Check to see if the event has a hit collection.
+            if(current.hasCollection(CalorimeterHit.class, hitCollectionName)) {
+                System.out.println("Has hits!");
+                // Get the list of LCIO hits.
+                List<CalorimeterHit> lcioList = current.get(CalorimeterHit.class, hitCollectionName);
+                
+                // Create a list to store event display hits.
+                List<EcalHit> displayList = new ArrayList<EcalHit>(lcioList.size());
+                
+                // Convert the LCIO clusters to display clusters.
+                for(CalorimeterHit lcioHit : lcioList) {
+                    displayList.add(toPanelHit(lcioHit));
+                }
+                
+                // Return the converted list of clusters.
+                return displayList;
+            }
+            
+            // If it does not, return an empty list.
+            else { return new ArrayList<EcalHit>(); }
+        }
+    }
+    
+    @Override
+    public boolean nextEvent() throws IOException {
+        // Try to read the next event.
+        try { current = reader.read(); }
+        
+        // If the read action fails, then there is no next event.
+        catch(IOException e) {
+            current = null;
+            return false;
+        }
+        
+        // Note that another event has been read.
+        eventsRead++;
+        
+        // Otherwise, indicate that an event was read.
+        return true;
+    }
+    
+    @Override
+    public boolean previousEvent() throws IOException {
+        // If we are on the first event, there is no previous event.
+        if(eventsRead == 0 || eventsRead == 1) { return false; }
+        
+        // Otherwise, reset the reader and skip to the previous event.
+        else {
+            // Create a new reader.
+            reader = new LCIOReader(sourceFile);
+            
+            // Skip to immediately before the previous event.
+            reader.skipEvents(eventsRead - 2);
+            
+            // Read the next event.
+            try { current = reader.read(); }
+            
+            // If the read fails, return false.
+            catch(IOException e) {
+                current = null;
+                return false;
+            }
+            
+            // Decrement the number of events read.
+            eventsRead--;
+            
+            // Otherwise, indicate that an event was read.
+            return true;
+        }
+    }
+    
+    public static final Cluster toPanelCluster(HPSEcalCluster lcioCluster) {
+        // If the argument is null, return null.
+        if(lcioCluster == null) { return null; }
+        
+        // Otherwise, get the cluster x/y indices and energy.
+        int ix = lcioCluster.getSeedHit().getIdentifierFieldValue("ix");
+        int iy = lcioCluster.getSeedHit().getIdentifierFieldValue("iy");
+        double energy = lcioCluster.getEnergy();
+        
+        // Create and return a panel cluster from the above values.
+        return new Cluster(ix, iy, energy);
+    }
+    
+    public static final EcalHit toPanelHit(CalorimeterHit lcioHit) {
+        // If the argument is null, return null.
+        if(lcioHit == null) { return null; }
+        
+        // Otherwise, get the cluster x/y indices and energy.
+        int ix = lcioHit.getIdentifierFieldValue("ix");
+        int iy = lcioHit.getIdentifierFieldValue("iy");
+        double energy = lcioHit.getCorrectedEnergy();
+        
+        // Create and return a panel hit from the above values.
+        return new EcalHit(ix, iy, energy);
+    }
+}

Modified: java/trunk/ecal-event-display/src/main/java/org/hps/monitoring/ecal/eventdisplay/io/TextManager.java
 =============================================================================
--- java/trunk/ecal-event-display/src/main/java/org/hps/monitoring/ecal/eventdisplay/io/TextManager.java	(original)
+++ java/trunk/ecal-event-display/src/main/java/org/hps/monitoring/ecal/eventdisplay/io/TextManager.java	Tue Nov  4 10:19:31 2014
@@ -1,5 +1,6 @@
 package org.hps.monitoring.ecal.eventdisplay.io;
 
+import java.io.File;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.StringTokenizer;
@@ -37,10 +38,10 @@
  * Indicates that the event has ended.
  * 
  * @author Kyle McCarty
- **/
+ */
 public final class TextManager implements EventManager {
     // File reader for reading the input.
-	private AdvancedReader reader;
+    private AdvancedReader reader;
     // List for storing the hits from the current event.
     private ArrayList<EcalHit> hitList = new ArrayList<EcalHit>();
     // List for storing the clusters from the current hit.
@@ -51,34 +52,45 @@
     private int curEvent = 0;
     
     /**
-     * <b>EventManager</b><br/><br/>
-     * <code>public <b>EventManager</b>(String filename)</code><br/><br/>
      * Initializes an event manager that will read from the indicated file.
      * @param filename - The path to the file containing hit information.
-     **/
+     */
+    public TextManager(File file) throws IOException {
+        reader = new AdvancedReader(file);
+    }
+    
+    /**
+     * Initializes an event manager that will read from the indicated file.
+     * @param filename - The path to the file containing hit information.
+     */
     public TextManager(String filename) throws IOException {
-    	reader = new AdvancedReader(filename);
-    }
-    
+        reader = new AdvancedReader(filename);
+    }
+    
+    @Override
     public void close() throws IOException {
         reader.close();
         open = false;
     }
     
+    @Override
     public int getEventNumber() {
-    	return curEvent;
-    }
-    
+        return curEvent;
+    }
+    
+    @Override
     public ArrayList<Cluster> getClusters() {
         if (!open) { return null; }
         else { return clusterList; }
     }
     
+    @Override
     public ArrayList<EcalHit> getHits() {
         if (!open) { return null; }
         else { return hitList; }
     }
     
+    @Override
     public boolean nextEvent() throws IOException {
         // We can only read of the reader is open.
         if (!open) { return false; }
@@ -115,12 +127,12 @@
             
             // If this is a cluster, add a new cluster object.
             if (name.compareTo("Cluster") == 0) {
-            	// Get the cluster energy, if it is given.
-            	double clusterEnergy = Double.NaN;
-            	if(st.hasMoreTokens()) { clusterEnergy = Double.parseDouble(st.nextToken()); }
-            	
-            	// Add a new cluster.
-            	clusterList.add(new Cluster(ix, iy, clusterEnergy));
+                // Get the cluster energy, if it is given.
+                double clusterEnergy = Double.NaN;
+                if(st.hasMoreTokens()) { clusterEnergy = Double.parseDouble(st.nextToken()); }
+                
+                // Add a new cluster.
+                clusterList.add(new Cluster(ix, iy, clusterEnergy));
             }
             
             // If this is a calorimeter hit, add a new calorimeter hit object.
@@ -131,24 +143,24 @@
             
             // If this is a cluster component hit, add it to the last cluster.
             else if(name.compareTo("CompHit") == 0) {
-            	// There must be a last cluster to process this hit type.
-            	if(clusterList.size() == 0) {
-            		System.err.println("File Format Error: A cluster component hit was read, but" +
-            				" no cluster has been declared. Terminating.");
-            		System.exit(1);
-            	}
-            	else { clusterList.get(clusterList.size() - 1).addComponentHit(ix, iy); }
+                // There must be a last cluster to process this hit type.
+                if(clusterList.size() == 0) {
+                    System.err.println("File Format Error: A cluster component hit was read, but" +
+                            " no cluster has been declared. Terminating.");
+                    System.exit(1);
+                }
+                else { clusterList.get(clusterList.size() - 1).addComponentHit(ix, iy); }
             }
             
             // If this is a cluster shared hit, add it to the last cluster.
             else if(name.compareTo("SharHit") == 0) {
-            	// There must be a last cluster to process this hit type.
-            	if(clusterList.size() == 0) {
-            		System.err.println("File Format Error: A cluster shared hit was read, but" +
-            				" no cluster has been declared. Terminating.");
-            		System.exit(1);
-            	}
-            	else { clusterList.get(clusterList.size() - 1).addSharedHit(ix, iy); }
+                // There must be a last cluster to process this hit type.
+                if(clusterList.size() == 0) {
+                    System.err.println("File Format Error: A cluster shared hit was read, but" +
+                            " no cluster has been declared. Terminating.");
+                    System.exit(1);
+                }
+                else { clusterList.get(clusterList.size() - 1).addSharedHit(ix, iy); }
             }
             
             // Get the next line.
@@ -159,37 +171,38 @@
         return true;
     }
     
+    @Override
     public boolean previousEvent() throws IOException {
-    	// If we are at the first event, do nothing. There is no
-    	// previous event to display.
-    	if(curEvent == 1) { return false; }
-    	
-    	// Otherwise, loop backward until we find the previous event header.
-    	String curLine;
-    	while(true) {
-    		// Get the previous line.
-    		curLine = reader.readPreviousLine();
-    		
-    		// Otherwise, if it is null, we've reached the start of the file.
-    		if(curLine == null) {
-    			System.err.println("Error: Unexpectedly reached SOF.");
-    			System.exit(1);
-    		}
-    		
-    		// If the previous line is an event, note it.
-    		if(curLine.substring(0, 5).compareTo("Event") == 0) {
-    	        // Get the event number of the current event.
-    	        StringTokenizer et = new StringTokenizer(curLine);
-    	        et.nextToken();
-    	        int readEvent = Integer.parseInt(et.nextToken());
-    	        
-    	        // If the read event number is one back from the current
-    	        // event, jump back a step and read the event.
-    	        if(readEvent == (curEvent - 1)) {
-    	        	reader.readPreviousLine();
-    	        	return nextEvent();
-    	        }
-    		}
-    	}
-    }
-}
+        // If we are at the first event, do nothing. There is no
+        // previous event to display.
+        if(curEvent == 1) { return false; }
+        
+        // Otherwise, loop backward until we find the previous event header.
+        String curLine;
+        while(true) {
+            // Get the previous line.
+            curLine = reader.readPreviousLine();
+            
+            // Otherwise, if it is null, we've reached the start of the file.
+            if(curLine == null) {
+                System.err.println("Error: Unexpectedly reached SOF.");
+                System.exit(1);
+            }
+            
+            // If the previous line is an event, note it.
+            if(curLine.substring(0, 5).compareTo("Event") == 0) {
+                // Get the event number of the current event.
+                StringTokenizer et = new StringTokenizer(curLine);
+                et.nextToken();
+                int readEvent = Integer.parseInt(et.nextToken());
+                
+                // If the read event number is one back from the current
+                // event, jump back a step and read the event.
+                if(readEvent == (curEvent - 1)) {
+                    reader.readPreviousLine();
+                    return nextEvent();
+                }
+            }
+        }
+    }
+}

Modified: java/trunk/ecal-event-display/src/main/java/org/hps/monitoring/ecal/eventdisplay/ui/ActiveViewer.java
 =============================================================================
--- java/trunk/ecal-event-display/src/main/java/org/hps/monitoring/ecal/eventdisplay/ui/ActiveViewer.java	(original)
+++ java/trunk/ecal-event-display/src/main/java/org/hps/monitoring/ecal/eventdisplay/ui/ActiveViewer.java	Tue Nov  4 10:19:31 2014
@@ -1,13 +1,12 @@
 package org.hps.monitoring.ecal.eventdisplay.ui;
 
-import java.awt.Color;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.KeyAdapter;
 import java.awt.event.KeyEvent;
-import java.awt.event.KeyListener;
-import java.awt.image.BufferedImage;
-import java.io.File;
 import java.io.IOException;
 
-import javax.imageio.ImageIO;
+import javax.swing.JMenuItem;
 
 import org.hps.monitoring.ecal.eventdisplay.io.EventManager;
 
@@ -15,15 +14,20 @@
  * Abstract class <code>ActiveViewer</code> describes a <code>Viewer
  * </code> object that is connected to a static data source. It is
  * designed to instruct the data source when to provide new events.
+ * <br/><br/>
+ * <code>ActiveViewer</code> is now superseded by <code>FileViewer
+ * </code>, which functions on its own. Any additional file types that
+ * should be supported should be added directly to <code>FileViewer
+ * </code> or a subclass. <code>ActiveViewer</code> will be removed
+ * from coming releases.
  * 
  * @author Kyle McCarty
  */
+@Deprecated
 public abstract class ActiveViewer extends Viewer {
-    private static final long serialVersionUID = -6107646224627009923L;
-    // Stores whether the background color is set or not.
-    private boolean background = false;
+    private static final long serialVersionUID = 2L;
     // Gets events from some file.
-    protected final EventManager em;
+    protected EventManager em;
     
     /**
      * Creates an active-type <code>Viewer</code> window which draws
@@ -39,6 +43,15 @@
         
         // Set the data source.
         this.em = em;
+        
+        // Add an exit command to the file menu.
+        JMenuItem menuExit = new JMenuItem("Exit", KeyEvent.VK_X);
+        menuExit.addActionListener(new ActionListener() {
+            @Override
+            public void actionPerformed(ActionEvent e) { dispose(); }
+        });
+        menu[MENU_FILE].addSeparator();
+        menu[MENU_FILE].add(menuExit);
         
         // Make a key listener to change events.
         addKeyListener(new EcalKeyListener());
@@ -59,16 +72,11 @@
     /**
      * The <code>EcalListener</code> class binds keys to actions.
      * Bound actions include:
-     * [Right Arrow] :: Next event (stand-alone mode only)
-     * [Left Arrow ] :: Previous event (stand-alone mode only)
-     * b             :: Toggle color-mapping for 0 energy crystals
-     * h             :: Toggle selected crystal highlighting
-     * l             :: Toggle logarithmic versus linear scaling
-     * s             :: Saves the current display to a file
+     * [Right Arrow] :: Next event
+     * [Left Arrow ] :: Previous event
      **/
-    private class EcalKeyListener implements KeyListener {
-        public void keyPressed(KeyEvent e) { }
-        
+    private class EcalKeyListener extends KeyAdapter {
+        @Override
         public void keyReleased(KeyEvent e) {
             // If right-arrow was pressed, go to the next event.
             if (e.getKeyCode() == 39) {
@@ -79,7 +87,7 @@
                 }
             }
             
-            // If right-arrow was pressed, go to the next event.
+            // If left-arrow was pressed, go to the next event.
             else if (e.getKeyCode() == 37) {
                 try { displayPreviousEvent(); }
                 catch (IOException ex) {
@@ -88,63 +96,8 @@
                 }
             }
             
-            // 'b' toggles the default white background.
-            else if(e.getKeyCode() == 66) {
-                if(background) { ecalPanel.setDefaultCrystalColor(null); }
-                else { ecalPanel.setDefaultCrystalColor(Color.GRAY); }
-                background = !background;
-            }
-            
-            // 'h' toggles highlighting the crystal under the cursor.
-            else if(e.getKeyCode() == 72) { ecalPanel.setSelectionHighlighting(!ecalPanel.isSelectionEnabled()); }
-            
-            // 'l' toggles linear or logarithmic scaling.
-            else if(e.getKeyCode() == 76) {
-                if(ecalPanel.isScalingLinear()) { ecalPanel.setScalingLogarithmic(); }
-                else { ecalPanel.setScalingLinear(); }
-            }
-            
-            // 'x' toggles x-axis mirroring.
-            else if(e.getKeyCode() == 88) {
-                ecalPanel.setMirrorX(!ecalPanel.isMirroredX());
-                updateStatusPanel();
-            }
-            
-            // 'y' toggles y-axis mirroring.
-            else if(e.getKeyCode() == 89) {
-                ecalPanel.setMirrorY(!ecalPanel.isMirroredY());
-                updateStatusPanel();
-            }
-            
-            // 's' saves the panel to a file.
-            else if(e.getKeyCode() == 83) {
-                // Make a new buffered image on which to draw the content pane.
-                BufferedImage screenshot = new BufferedImage(getContentPane().getWidth(),
-                        getContentPane().getHeight(), BufferedImage.TYPE_INT_ARGB);
-                
-                // Paint the content pane to image.
-                getContentPane().paint(screenshot.getGraphics());
-                
-                // Get the lowest available file name.
-                int fileNum = 0;
-                File imageFile = new File("screenshot_" + fileNum + ".png");
-                while(imageFile.exists()) {
-                    fileNum++;
-                    imageFile = new File("screenshot_" + fileNum + ".png");
-                }
-                
-                // Save the image to a PNG file.
-                try { ImageIO.write(screenshot, "PNG", imageFile); }
-                catch(IOException ioe) {
-                    System.err.println("Error saving file \"screenshot.png\".");
-                }
-                System.out.println("Screenshot saved to: " + imageFile.getAbsolutePath());
-            }
-            
             // Otherwise, print out the key code for the pressed key.
             else { System.out.printf("Key Code: %d%n", e.getKeyCode()); }
         }
-        
-        public void keyTyped(KeyEvent e) { }
     }
-}
+}

Modified: java/trunk/ecal-event-display/src/main/java/org/hps/monitoring/ecal/eventdisplay/ui/CalorimeterPanel.java
 =============================================================================
--- java/trunk/ecal-event-display/src/main/java/org/hps/monitoring/ecal/eventdisplay/ui/CalorimeterPanel.java	(original)
+++ java/trunk/ecal-event-display/src/main/java/org/hps/monitoring/ecal/eventdisplay/ui/CalorimeterPanel.java	Tue Nov  4 10:19:31 2014
@@ -5,16 +5,22 @@
 import java.awt.FontMetrics;
 import java.awt.Graphics;
 import java.awt.Point;
+import java.awt.event.ComponentAdapter;
+import java.awt.event.ComponentEvent;
 import java.text.DecimalFormat;
 import java.text.NumberFormat;
 import java.util.ArrayList;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Set;
 
 import javax.swing.JPanel;
 
 import org.hps.monitoring.ecal.eventdisplay.event.Association;
+import org.hps.monitoring.ecal.eventdisplay.util.ColorScale;
 import org.hps.monitoring.ecal.eventdisplay.util.MultiGradientScale;
+import org.hps.monitoring.ecal.eventdisplay.util.SettingsEvent;
+import org.hps.monitoring.ecal.eventdisplay.util.SettingsListener;
 
 /**
  * The class <code>CalorimeterPanel</code> handles the rendering of the
@@ -22,24 +28,24 @@
  * rendering a color scale, and marking cluster crystals.
  * 
  * @author Kyle McCarty
- **/
+ */
 public final class CalorimeterPanel extends JPanel {
     // Java-suggested variable.
-    private static final long serialVersionUID = 6292751227464151897L;
+    private static final long serialVersionUID = 2L;
     // The color used for rendering seed hits.
     private Color clusterColor = Color.GREEN;
     // The default color of the calorimeter crystals.
     private Color defaultColor = null;
     // The color if disabled crystals.
     private Color disabledColor = Color.BLACK;
-	// The color for the selected crystal.
-	private Color selectedColor = new Color(90, 170, 250);
-	// Whether to allow highlighting of the selected crystal.
-	private boolean enabledSelection = true;
-	// Whether the crystals should redraw automatically or not.
-	private boolean suppress = false;
+    // The color for the selected crystal.
+    private Color selectedColor = new Color(90, 170, 250);
+    // Whether to allow highlighting of the selected crystal.
+    private boolean enabledSelection = true;
+    // Whether the crystals should redraw automatically or not.
+    private boolean suppress = false;
     // The color-mapping scale used by to color calorimeter crystals.
-    private MultiGradientScale scale = MultiGradientScale.makeRainbowScale(0.0, 1.0);
+    private ColorScale scale = MultiGradientScale.makeRainbowScale(0.0, 1.0);
     // The number of boxes in the x-direction.
     private int xBoxes = 1;
     // The number of boxes in the y-direction.
@@ -58,6 +64,8 @@
     private Point selectedCrystal = null;
     // The panel on which the scale is rendered.
     private ScalePanel scalePanel = new ScalePanel();
+    // Store all settings listeners.
+    private List<SettingsListener> listenerList = new ArrayList<SettingsListener>();
     
     // Efficiency variables for crystal placement.
     private int[] xPosition;
@@ -72,7 +80,7 @@
      * Initializes the calorimeter panel.
      * @param numXBoxes - The number of crystals in the x-direction.
      * @param numYBoxes - The number of crystals in the y-direction.
-     **/
+     */
     public CalorimeterPanel(int numXBoxes, int numYBoxes) {
         // Initialize the base component.
         super();
@@ -85,10 +93,10 @@
         crystal = new Crystal[xBoxes][yBoxes];
         
         for(int x = 0; x < xBoxes; x++) {
-        	for(int y = 0; y < yBoxes; y++) {
-        		crystal[x][y] = new Crystal();
-        		add(crystal[x][y]);
-        	}
+            for(int y = 0; y < yBoxes; y++) {
+                crystal[x][y] = new Crystal();
+                add(crystal[x][y]);
+            }
         }
         
         // Initialize the size arrays,
@@ -98,6 +106,14 @@
         // Add the scale panel.
         setLayout(null);
         add(scalePanel);
+        
+        // Add a component listener to trigger resizing.
+        addComponentListener(new ComponentAdapter() {
+            @Override
+            public void componentResized(ComponentEvent e) {
+                resize();
+            }
+        });
     }
     
     // ================================================================================
@@ -114,18 +130,18 @@
      * not correspond to a crystal.
      */
     public void addAssociation(Association crystalAssociation) throws IndexOutOfBoundsException {
-    	// Validate the parent crystal's indices.
-    	Point parent = crystalAssociation.getParentCrystal();
-    	if(validateIndices(parent)) {
-        	// Validate the parent crystal's indices.
-    		Point child = crystalAssociation.getChildCrystal();
-    		if(validateIndices(child)) {
-    			// If both crystal index sets are valid, add the association.
-    			crystal[parent.x][parent.y].addAssociation(crystalAssociation);
-    		}
-        	else { throw new IndexOutOfBoundsException(String.format("Invalid child crystal address (%2d, %2d).", parent.x, parent.y)); }
-    	}
-    	else { throw new IndexOutOfBoundsException(String.format("Invalid parent crystal address (%2d, %2d).", parent.x, parent.y)); }
+        // Validate the parent crystal's indices.
+        Point parent = crystalAssociation.getParentCrystal();
+        if(validateIndices(parent)) {
+            // Validate the parent crystal's indices.
+            Point child = crystalAssociation.getChildCrystal();
+            if(validateIndices(child)) {
+                // If both crystal index sets are valid, add the association.
+                crystal[parent.x][parent.y].addAssociation(crystalAssociation);
+            }
+            else { throw new IndexOutOfBoundsException(String.format("Invalid child crystal address (%2d, %2d).", parent.x, parent.y)); }
+        }
+        else { throw new IndexOutOfBoundsException(String.format("Invalid parent crystal address (%2d, %2d).", parent.x, parent.y)); }
     }
     
     /**
@@ -136,12 +152,12 @@
      * @param energy - The energy to add.
      * @throws IndexOutOfBoundsException Occurs when either of the given
      * x/y crystal coordinates do not correspond to a crystal.
-     **/
+     */
     public void addCrystalEnergy(int ix, int iy, double energy) throws IndexOutOfBoundsException {
-    	// Mirror the coordinates if appropriate.
-    	if(mirrorX) { ix = getMirroredX(ix); }
-    	if(mirrorY) { iy = getMirroredY(iy); }
-    	
+        // Mirror the coordinates if appropriate.
+        if(mirrorX) { ix = getMirroredX(ix); }
+        if(mirrorY) { iy = getMirroredY(iy); }
+        
         if (validateIndices(ix, iy)) { crystal[ix][iy].addEnergy(energy); }
         else { throw new IndexOutOfBoundsException(String.format("Invalid crystal address (%2d, %2d).", ix, iy)); }
     }
@@ -153,9 +169,17 @@
      * @param energy - The energy to add.
      * @throws IndexOutOfBoundsException Occurs when either of the given
      * x/y crystal coordinates do not correspond to a crystal.
-     **/
+     */
     public void addCrystalEnergy(Point ixy, double energy) throws IndexOutOfBoundsException {
         addCrystalEnergy(ixy.x, ixy.y, energy);
+    }
+    
+    /**
+     * Adds a settings listener to the object.
+     * @param listener - The listener to add.
+     */
+    public void addSettingsListener(SettingsListener listener) {
+        if(listener != null) { listenerList.add(listener); }
     }
     
     /**
@@ -163,35 +187,35 @@
      * from slightly under the smallest recorded value to the highest
      * recorded value.
      */
-	public void autoScale() {
-		// If there is no energy on the calorimeter, just set the
-		// scale to some default value.
-		if(extremum[0] == Double.MAX_VALUE && extremum[1] == 0.0) {
-			scale.setMaximum(1.0);
-			scale.setMinimum(0.01);
-		}
-		
-		// If the minimum is defined, set the scale such that it
-		// will map that value to the low end and the highest value
-		// to the top.
-		else if(extremum[0] != Double.MAX_VALUE) {
-			scale.setMaximum(extremum[1]);
-			scale.setMinimum(extremum[0] / 2);
-		}
-	}
+    public void autoScale() {
+        // If there is no energy on the calorimeter, just set the
+        // scale to some default value.
+        if(extremum[0] == Double.MAX_VALUE && extremum[1] == 0.0) {
+            scale.setMaximum(1.0);
+            scale.setMinimum(0.01);
+        }
+        
+        // If the minimum is defined, set the scale such that it
+        // will map that value to the low end and the highest value
+        // to the top.
+        else if(extremum[0] != Double.MAX_VALUE) {
+            scale.setMaximum(extremum[1]);
+            scale.setMinimum(extremum[0] / 2);
+        }
+    }
     
     /**
      * Sets all crystal energies to zero, removes all clusters, and
      * clears all highlighting. This <b>does not</b> enable disabled
      * crystals.
-     **/
+     */
     public void clearCrystals() {
         for (int ix = 0; ix < xBoxes; ix++) {
             for (int iy = 0; iy < yBoxes; iy++) {
-            	crystal[ix][iy].setState(0.0, false, null);
-            	crystal[ix][iy].clearAssociations();
-            	extremum[0] = Double.MAX_VALUE;
-            	extremum[1] = 0.0;
+                crystal[ix][iy].setState(0.0, false, null);
+                crystal[ix][iy].clearAssociations();
+                extremum[0] = Double.MAX_VALUE;
+                extremum[1] = 0.0;
             }
         }
     }
@@ -209,11 +233,11 @@
      * Clears crystal selection and sets it to <code>null</code>.
      */
     public void clearSelectedCrystal() {
-    	// Clear any currently selected crystals.
-    	if(selectedCrystal != null) { crystal[selectedCrystal.x][selectedCrystal.y].setSelected(false); }
-    	
-    	// Set the selected crystal to null.
-    	selectedCrystal = null;
+        // Clear any currently selected crystals.
+        if(selectedCrystal != null) { crystal[selectedCrystal.x][selectedCrystal.y].setSelected(false); }
+        
+        // Set the selected crystal to null.
+        selectedCrystal = null;
     }
     
     /**
@@ -225,11 +249,11 @@
      * x/y crystal coordinates do not correspond to a crystal.
      */
     public double getCrystalEnergy(int ix, int iy) throws IndexOutOfBoundsException {
-    	// Mirror the coordinates if appropriate.
-    	if(mirrorX) { ix = getMirroredX(ix); }
-    	if(mirrorY) { iy = getMirroredY(iy); }
-    	
-    	if(validateIndices(ix, iy)) { return crystal[ix][iy].getEnergy(); }
+        // Mirror the coordinates if appropriate.
+        if(mirrorX) { ix = getMirroredX(ix); }
+        if(mirrorY) { iy = getMirroredY(iy); }
+        
+        if(validateIndices(ix, iy)) { return crystal[ix][iy].getEnergy(); }
         else { throw new IndexOutOfBoundsException(String.format("Invalid crystal address (%2d, %2d).", ix, iy)); }
     }
     
@@ -241,7 +265,7 @@
      * x/y crystal coordinates do not correspond to a crystal.
      */
     public double getCrystalEnergy(Point ixy) throws IndexOutOfBoundsException {
-    	return getCrystalEnergy(ixy.x, ixy.y);
+        return getCrystalEnergy(ixy.x, ixy.y);
     }
     
     /**
@@ -254,11 +278,11 @@
      * x/y crystal coordinates do not correspond to a crystal.
      */
     public Color getCrystalHighlight(int ix, int iy) throws IndexOutOfBoundsException {
-    	// Mirror the coordinates if appropriate.
-    	if(mirrorX) { ix = getMirroredX(ix); }
-    	if(mirrorY) { iy = getMirroredY(iy); }
-    	
-    	if(validateIndices(ix, iy)) { return crystal[ix][iy].getHighlight(); }
+        // Mirror the coordinates if appropriate.
+        if(mirrorX) { ix = getMirroredX(ix); }
+        if(mirrorY) { iy = getMirroredY(iy); }
+        
+        if(validateIndices(ix, iy)) { return crystal[ix][iy].getHighlight(); }
         else { throw new IndexOutOfBoundsException(String.format("Invalid crystal address (%2d, %2d).", ix, iy)); }
     }
     
@@ -271,7 +295,7 @@
      * x/y crystal coordinates do not correspond to a crystal.
      */
     public Color getCrystalHighlight(Point ixy) throws IndexOutOfBoundsException {
-    	return getCrystalHighlight(ixy.x, ixy.y);
+        return getCrystalHighlight(ixy.x, ixy.y);
     }
     
     /**
@@ -284,38 +308,38 @@
      * Returns <code>null</code> if the coordinates do not map to a crystal.
      */
     public Point getCrystalID(int xCoor, int yCoor) {
-    	// If either coordinate is negative, return the null result.
-    	if(xCoor < 0 || yCoor < 0) { return null; }
-    	
-    	// If either coordinate is too large, return the nul result.
-    	if(xCoor > xPosition[xBoxes] || yCoor > yPosition[yBoxes]) {
-    		return null;
-    	}
-    	
-    	// Make a point to identify the crystal index.
-    	Point loc = new Point(-1, -1);
-    	
-    	// Determine which y index it is.
-    	for(int y = 0; y < yBoxes; y++) {
-    		if(yCoor <= yPosition[y + 1]) {
-    			loc.y = y;
-    			break;
-    		}
-    	}
-    	
-    	// Determine which x index it is.
-    	for(int x = 0; x < xBoxes; x++) {
-    		if(xCoor <= xPosition[x + 1]) {
-    			loc.x = x;
-    			break;
-    		}
-    	}
-    	
-    	// If either coordinate is not valid, return null.
-    	if(loc.x == -1 || loc.y == -1) { return null; }
-    	
-    	// Return the crystal identifier.
-    	return getMirroredPoint(loc, mirrorX, mirrorY);
+        // If either coordinate is negative, return the null result.
+        if(xCoor < 0 || yCoor < 0) { return null; }
+        
+        // If either coordinate is too large, return the nul result.
+        if(xCoor > xPosition[xBoxes] || yCoor > yPosition[yBoxes]) {
+            return null;
+        }
+        
+        // Make a point to identify the crystal index.
+        Point loc = new Point(-1, -1);
+        
+        // Determine which y index it is.
+        for(int y = 0; y < yBoxes; y++) {
+            if(yCoor <= yPosition[y + 1]) {
+                loc.y = y;
+                break;
+            }
+        }
+        
+        // Determine which x index it is.
+        for(int x = 0; x < xBoxes; x++) {
+            if(xCoor <= xPosition[x + 1]) {
+                loc.x = x;
+                break;
+            }
+        }
+        
+        // If either coordinate is not valid, return null.
+        if(loc.x == -1 || loc.y == -1) { return null; }
+        
+        // Return the crystal identifier.
+        return getMirroredPoint(loc, mirrorX, mirrorY);
     }
     
     /**
@@ -350,6 +374,13 @@
     public Set<Point> getNeighbors(int cix, int ciy) { return getNeighbors(new Point(cix, ciy)); }
     
     /**
+     * Gets the color that will be used for zero-energy crystals.
+     * @return Returns the zero-energy crystal color as a <code>Color
+     * </code> object.
+     */
+    public Color getDefaultCrystalColor() { return defaultColor; }
+    
+    /**
      * Gets the set of valid crystals that immediately surround the
      * central crystal. Valid crystals must both have valid indices
      * and also be enabled.
@@ -361,43 +392,43 @@
      * x/y central crystal coordinates do not correspond to a crystal.
      */
     public Set<Point> getNeighbors(Point centralCrystal) throws IndexOutOfBoundsException {
-    	// Mirror the coordinates if appropriate.
-    	centralCrystal = getMirroredPoint(centralCrystal, mirrorX, mirrorY);
-    	
-    	// Make sure that the root is a valid crystal.
-    	if(!validateIndices(centralCrystal)) {
+        // Mirror the coordinates if appropriate.
+        centralCrystal = getMirroredPoint(centralCrystal, mirrorX, mirrorY);
+        
+        // Make sure that the root is a valid crystal.
+        if(!validateIndices(centralCrystal)) {
             throw new IndexOutOfBoundsException(String.format("Invalid central crystal address (%2d, %2d).",
-            		centralCrystal.x, centralCrystal.y));
-    	}
-    	
-    	// Make a set to store the neighbors in.
-    	HashSet<Point> neighborSet = new HashSet<Point>();
-    	
-    	// Check all the crystals in a 3-by-3 square around the root.
-    	// If they are valid crystals and they are not disable, then
-    	// they are neighbors.
-    	for(int xMod = -1; xMod <= 1; xMod++) {
-    		for(int yMod = -1; yMod <= 1; yMod++) {
-    			// Get the possible neighbor.
-    			Point possibleNeighbor = new Point(centralCrystal.x + xMod, centralCrystal.y + yMod);
-    			
-    			// Make sure that the possible neighbor is neither the
-    			// root crystal nor invalid.
-    			boolean isRoot = centralCrystal.equals(possibleNeighbor);
-    			boolean isValid = validateIndices(possibleNeighbor);
-    			
-    			// If the neighbor passes these tests, add it to the set
-    			// if it is active.
-    			if(!isRoot && isValid) {
-    				if(!crystal[possibleNeighbor.x][possibleNeighbor.y].isDisabled()) {
-    					neighborSet.add(getMirroredPoint(possibleNeighbor, mirrorX, mirrorY));
-    				}
-    			}
-    		}
-    	}
-    	
-    	// Return the neighbor set.
-    	return neighborSet;
+                    centralCrystal.x, centralCrystal.y));
+        }
+        
+        // Make a set to store the neighbors in.
+        HashSet<Point> neighborSet = new HashSet<Point>();
+        
+        // Check all the crystals in a 3-by-3 square around the root.
+        // If they are valid crystals and they are not disable, then
+        // they are neighbors.
+        for(int xMod = -1; xMod <= 1; xMod++) {
+            for(int yMod = -1; yMod <= 1; yMod++) {
+                // Get the possible neighbor.
+                Point possibleNeighbor = new Point(centralCrystal.x + xMod, centralCrystal.y + yMod);
+                
+                // Make sure that the possible neighbor is neither the
+                // root crystal nor invalid.
+                boolean isRoot = centralCrystal.equals(possibleNeighbor);
+                boolean isValid = validateIndices(possibleNeighbor);
+                
+                // If the neighbor passes these tests, add it to the set
+                // if it is active.
+                if(!isRoot && isValid) {
+                    if(!crystal[possibleNeighbor.x][possibleNeighbor.y].isDisabled()) {
+                        neighborSet.add(getMirroredPoint(possibleNeighbor, mirrorX, mirrorY));
+                    }
+                }
+            }
+        }
+        
+        // Return the neighbor set.
+        return neighborSet;
     }
     
     /**
@@ -406,6 +437,16 @@
      * If no crystal is currently selected, returns <code>null</code>.
      */
     public Point getSelectedCrystal() { return getMirroredPoint(selectedCrystal, mirrorX, mirrorY); }
+    
+    /**
+     * Gets the list of <code>SettingsListener</code> objects attached
+     * to the component.
+     * @return Returns the object's <code>SettingsListener</code> objects
+     * in an array.
+     */
+    public SettingsListener[] getSettingsListeners() {
+        return listenerList.toArray(new SettingsListener[listenerList.size()]);
+    }
     
     /**
      * Determines if the crystal at the given coordinates is a cluster
@@ -419,11 +460,11 @@
      * x/y crystal coordinates do not correspond to a crystal.
      */
     public boolean isCrystalCluster(int ix, int iy) throws IndexOutOfBoundsException {
-    	// Mirror the coordinates if appropriate.
-    	if(mirrorX) { ix = getMirroredX(ix); }
-    	if(mirrorY) { iy = getMirroredY(iy); }
-    	
-    	if(validateIndices(ix, iy)) { return crystal[ix][iy].isClusterCenter(); }
+        // Mirror the coordinates if appropriate.
+        if(mirrorX) { ix = getMirroredX(ix); }
+        if(mirrorY) { iy = getMirroredY(iy); }
+        
+        if(validateIndices(ix, iy)) { return crystal[ix][iy].isClusterCenter(); }
         else { throw new IndexOutOfBoundsException(String.format("Invalid crystal address (%2d, %2d).", ix, iy)); }
     }
     
@@ -438,7 +479,7 @@
      * x/y crystal coordinates do not correspond to a crystal.
      */
     public boolean isCrystalCluster(Point ixy) throws IndexOutOfBoundsException {
-    	return isCrystalCluster(ixy.x, ixy.y);
+        return isCrystalCluster(ixy.x, ixy.y);
     }
     
     /**
@@ -452,11 +493,11 @@
      * x/y crystal coordinates do not correspond to a crystal.
      */
     public boolean isCrystalDisabled(int ix, int iy) throws IndexOutOfBoundsException {
-    	// Mirror the coordinates if appropriate.
-    	if(mirrorX) { ix = getMirroredX(ix); }
-    	if(mirrorY) { iy = getMirroredY(iy); }
-    	
-    	if(validateIndices(ix, iy)) { return crystal[ix][iy].isDisabled(); }
+        // Mirror the coordinates if appropriate.
+        if(mirrorX) { ix = getMirroredX(ix); }
+        if(mirrorY) { iy = getMirroredY(iy); }
+        
+        if(validateIndices(ix, iy)) { return crystal[ix][iy].isDisabled(); }
         else { throw new IndexOutOfBoundsException(String.format("Invalid crystal address (%2d, %2d).", ix, iy)); }
     }
     
@@ -470,7 +511,7 @@
      * x/y crystal coordinates do not correspond to a crystal.
      */
     public boolean isCrystalDisabled(Point ixy) throws IndexOutOfBoundsException {
-    	return isCrystalDisabled(ixy.x, ixy.y);
+        return isCrystalDisabled(ixy.x, ixy.y);
     }
     
     public boolean isMirroredX() { return mirrorX; }
@@ -500,11 +541,19 @@
     public boolean isSelectionEnabled() { return enabledSelection; }
     
     /**
+     * Removes the indicated listener from the object if it exists.
+     * @param listener - The listener to remove.
+     */
+    public void removeSettingsListener(SettingsListener listener) {
+        if(listener != null) { listenerList.remove(listener); }
+    }
+    
+    /**
      * Sets the color of the cluster center marker.
      * @param c - The color to be used for cluster center markers. A
      * value of <code>null</code> will result in seed hit markers being
      * the inverse color of the crystal in which they appear.
-     **/
+     */
     public void setClusterColor(Color c) { clusterColor = c; }
     
     /**
@@ -515,12 +564,12 @@
      * is a cluster center and <code>false</code> if there is not.
      * @throws IndexOutOfBoundsException Occurs when either of the given
      * x/y crystal coordinates do not correspond to a crystal.
-     **/
+     */
     public void setCrystalCluster(int ix, int iy, boolean cluster) throws IndexOutOfBoundsException {
-    	// Mirror the coordinates if appropriate.
-    	if(mirrorX) { ix = getMirroredX(ix); }
-    	if(mirrorY) { iy = getMirroredY(iy); }
-    	
+        // Mirror the coordinates if appropriate.
+        if(mirrorX) { ix = getMirroredX(ix); }
+        if(mirrorY) { iy = getMirroredY(iy); }
+        
         if (validateIndices(ix, iy)) { crystal[ix][iy].setClusterCenter(cluster); }
         else { throw new IndexOutOfBoundsException(String.format("Invalid crystal address (%2d, %2d).", ix, iy)); }
     }
@@ -532,7 +581,7 @@
      * is a cluster center and <code>false</code> if there is not.
      * @throws IndexOutOfBoundsException Occurs when either of the given
      * x/y crystal coordinates do not correspond to a crystal.
-     **/
+     */
     public void setCrystalCluster(Point ixy, boolean cluster) throws IndexOutOfBoundsException {
         setCrystalCluster(ixy.x, ixy.y, cluster);
     }
@@ -545,12 +594,12 @@
      * active and <code>false</code> if it is not.
      * @throws IndexOutOfBoundsException Occurs when either of the given
      * x/y crystal coordinates do not correspond to a crystal.
-     **/
+     */
     public void setCrystalEnabled(int ix, int iy, boolean active) throws IndexOutOfBoundsException {
-    	// Mirror the coordinates if appropriate.
-    	if(mirrorX) { ix = getMirroredX(ix); }
-    	if(mirrorY) { iy = getMirroredY(iy); }
-    	
+        // Mirror the coordinates if appropriate.
+        if(mirrorX) { ix = getMirroredX(ix); }
+        if(mirrorY) { iy = getMirroredY(iy); }
+        
         if (validateIndices(ix, iy)) { crystal[ix][iy].setDisabled(!active); }
         else { throw new IndexOutOfBoundsException(String.format("Invalid crystal address (%2d, %2d).", ix, iy)); }
     }
@@ -562,7 +611,7 @@
      * active and <code>false</code> if it is not.
      * @throws IndexOutOfBoundsException Occurs when either of the given
      * x/y crystal coordinates do not correspond to a crystal.
-     **/
+     */
     public void setCrystalEnabled(Point ixy, boolean active) throws IndexOutOfBoundsException {
         setCrystalEnabled(ixy.x, ixy.y, active);
     }
@@ -577,10 +626,10 @@
      * x/y crystal coordinates do not correspond to a crystal.
      */
     public void setCrystalHighlight(int ix, int iy, Color highlight) throws IndexOutOfBoundsException {
-    	// Mirror the coordinates if appropriate.
-    	if(mirrorX) { ix = getMirroredX(ix); }
-    	if(mirrorY) { iy = getMirroredY(iy); }
-    	
+        // Mirror the coordinates if appropriate.
+        if(mirrorX) { ix = getMirroredX(ix); }
+        if(mirrorY) { iy = getMirroredY(iy); }
+        
         if (validateIndices(ix, iy)) { crystal[ix][iy].setHighlight(highlight); }
         else { throw new IndexOutOfBoundsException(String.format("Invalid crystal address (%2d, %2d).", ix, iy)); }
     }
@@ -604,20 +653,23 @@
      * map value.
      */
     public void setDefaultCrystalColor(Color c) {
-    	// Only update the crystals if the default color has changed.
-    	if(c != defaultColor) {
-    		// Store the new default color.
-    		defaultColor = c;
-    		
-    		// Inform the crystals of the change.
-    		for(int ix = 0; ix < xBoxes; ix++) {
-    			for(int iy = 0; iy < yBoxes; iy++) { crystal[ix][iy].setUseDefaultColor(c != null, false); }
-    		}
-    		
-    		// Reset the colors and repaint.
-    		resetCrystalColors();
-    		repaint();
-    	}
+        // Only update the crystals if the default color has changed.
+        if(c != defaultColor) {
+            // Store the new default color.
+            defaultColor = c;
+            
+            // Inform the crystals of the change.
+            for(int ix = 0; ix < xBoxes; ix++) {
+                for(int iy = 0; iy < yBoxes; iy++) { crystal[ix][iy].setUseDefaultColor(c != null, false); }
+            }
+            
+            // Reset the colors and repaint.
+            resetCrystalColors();
+            repaint();
+            
+            // Throw an event.
+            throwSettingsEvent(SettingsEvent.PROPERTY_ZERO_ENERGY_COLOR);
+        }
     }
     
     /**
@@ -627,45 +679,45 @@
      * @param mirrorY - <code>true</code> indicates that the y-axis
      * should be mirrored and <code>false</code> that it should not.
      */
-    public void setMirror(boolean mirrorX, boolean mirrorY) {    	
-    	// Determine whether a given axis should be flipped.
-    	boolean flipX = (this.mirrorX != mirrorX);
-    	boolean flipY = (this.mirrorY != mirrorY);
-    	
-    	// If the axes are already as requested, then no further action
-    	// is necessary.
-    	if(!(flipX || flipY)) { return; }
-    	
-    	// Otherwise, mirror as requested.
-    	this.mirrorX = mirrorX;
-    	this.mirrorY = mirrorY;
-    	
-    	// Loop over the crystals and move them to the appropriate
-    	// mirrored array position.
-    	Crystal[][] mirroredCrystal = new Crystal[xBoxes][yBoxes];
-    	for(int ix = 0; ix < xBoxes; ix++) {
-    		for(int iy = 0; iy < yBoxes; iy++) {
-    			// Mirror the coordinates as appropriate.
-    			int mix = ix;
-    			int miy = iy;
-    			if(flipX) { mix = getMirroredX(ix); }
-    			if(flipY) { miy = getMirroredY(iy); }
-    			
-    			// Place the original crystal in its new position.
-    			mirroredCrystal[mix][miy] = crystal[ix][iy];
-    		}
-    	}
-    	
-    	// Change the selected crystal to the new one.
-    	if(selectedCrystal != null) {
-	    	crystal[selectedCrystal.x][selectedCrystal.y].setSelected(false);
-	    	mirroredCrystal[selectedCrystal.x][selectedCrystal.y].setSelected(true);
-    	}
-    	
-    	
-    	// Replace the original crystal array with the new one and redraw.
-    	crystal = mirroredCrystal;
-    	setSize(getSize());
+    public void setMirror(boolean mirrorX, boolean mirrorY) {        
+        // Determine whether a given axis should be flipped.
+        boolean flipX = (this.mirrorX != mirrorX);
+        boolean flipY = (this.mirrorY != mirrorY);
+        
+        // If the axes are already as requested, then no further action
+        // is necessary.
+        if(!(flipX || flipY)) { return; }
+        
+        // Otherwise, mirror as requested.
+        this.mirrorX = mirrorX;
+        this.mirrorY = mirrorY;
+        
+        // Loop over the crystals and move them to the appropriate
+        // mirrored array position.
+        Crystal[][] mirroredCrystal = new Crystal[xBoxes][yBoxes];
+        for(int ix = 0; ix < xBoxes; ix++) {
+            for(int iy = 0; iy < yBoxes; iy++) {
+                // Mirror the coordinates as appropriate.
+                int mix = ix;
+                int miy = iy;
+                if(flipX) { mix = getMirroredX(ix); }
+                if(flipY) { miy = getMirroredY(iy); }
+                
+                // Place the original crystal in its new position.
+                mirroredCrystal[mix][miy] = crystal[ix][iy];
+            }
+        }
+        
+        // Change the selected crystal to the new one.
+        if(selectedCrystal != null) {
+            crystal[selectedCrystal.x][selectedCrystal.y].setSelected(false);
+            mirroredCrystal[selectedCrystal.x][selectedCrystal.y].setSelected(true);
+        }
+        
+        
+        // Replace the original crystal array with the new one and redraw.
+        crystal = mirroredCrystal;
+        resize();
     }
     
     /**
@@ -673,23 +725,39 @@
      * @param state - <code>true</code> indicates that the axis should
      * be mirrored and <code>false</code> that it should not.
      */
-    public void setMirrorX(boolean mirrorX) { setMirror(mirrorX, mirrorY); }
+    public void setMirrorX(boolean mirrorX) {
+            // Process the change.
+            setMirror(mirrorX, mirrorY);
+            
+            // Throw an event.
+            throwSettingsEvent(SettingsEvent.PROPERTY_X_ORIENTATION);
+    }
     
     /**
      * Sets whether to mirror the y-axis on the calorimeter display.
      * @param state - <code>true</code> indicates that the axis should
      * be mirrored and <code>false</code> that it should not.
      */
-    public void setMirrorY(boolean mirrorY) { setMirror(mirrorX, mirrorY); }
+    public void setMirrorY(boolean mirrorY) {
+            // Process the change.
+            setMirror(mirrorX, mirrorY);
+            
+            // Throw an event.
+            throwSettingsEvent(SettingsEvent.PROPERTY_Y_ORIENTATION);
+    }
     
     /**
      * Sets whether the scale should be visible or not.
      * @param enabled - <code>true</code> indicates that the scale should
      * be visible and <code>false</code> that it should be hidden.
-     **/
+     */
     public void setScaleEnabled(boolean enabled) {
         if (scalePanel.isVisible() != enabled) {
+            // Process the change.
             scalePanel.setVisible(enabled);
+            
+            // Throw an event.
+            throwSettingsEvent(SettingsEvent.PROPERTY_SCALE_VISIBLE);
         }
     }
     
@@ -697,36 +765,60 @@
      * Sets the maximum value of the color mapping scale. Energies above this
      * value will all be the same maximum color.
      * @param maximum - The maximum energy to be mapped.
-     **/
+     */
     public void setScaleMaximum(double maximum) {
-        scale.setMaximum(maximum);
+        if(scale.getMaximum() != maximum) {
+            // Process the change.
+            scale.setMaximum(maximum);
+            
+            // Throw an event.
+            throwSettingsEvent(SettingsEvent.PROPERTY_SCALE_RANGE);
+        }
     }
     
     /**
      * Sets the minimum value of the color mapping scale. Energies below this
      * value will all be the same minimum color.
      * @param minimum - The minimum energy to be mapped.
-     **/
+     */
     public void setScaleMinimum(double minimum) {
-        scale.setMinimum(minimum);
+        if(scale.getMinimum() != minimum) {
+            // Process the change.
+            scale.setMinimum(minimum);
+            
+            // Throw an event.
+            throwSettingsEvent(SettingsEvent.PROPERTY_SCALE_RANGE);
+        }
     }
     
     /**
      * Sets the color mapping scale behavior to linear mapping.
-     **/
+     */
     public void setScalingLinear() {
-        scale.setScalingLinear();
-        resetCrystalColors();
-        repaint();
+        if(!isScalingLinear()) {
+            // Process the change in scaling.
+            scale.setScalingLinear();
+            resetCrystalColors();
+            repaint();
+            
+            // Throw an event.
+            throwSettingsEvent(SettingsEvent.PROPERTY_SCALE_TYPE);
+        }
     }
     
     /**
      * Sets the color mapping scale behavior to logarithmic mapping.
-     **/
+     */
     public void setScalingLogarithmic() {
-        scale.setScalingLogarithmic();
-        resetCrystalColors();
-        repaint();
+        if(!isScalingLogarithmic()) {
+            // Process the change in scaling.
+            scale.setScalingLogarithmic();
+            resetCrystalColors();
+            repaint();
+            
+            // Throw an event.
+            throwSettingsEvent(SettingsEvent.PROPERTY_SCALE_TYPE);
+        }
     }
     
     /**
@@ -737,12 +829,12 @@
      * x/y crystal coordinates do not correspond to a crystal.
      */
     public void setSelectedCrystal(int ix, int iy) {
-    	// Mirror the coordinates if appropriate.
-    	if(mirrorX) { ix = getMirroredX(ix); }
-    	if(mirrorY) { iy = getMirroredY(iy); }
-    	
+        // Mirror the coordinates if appropriate.
+        if(mirrorX) { ix = getMirroredX(ix); }
+        if(mirrorY) { iy = getMirroredY(iy); }
+        
         if (validateIndices(ix, iy)) {
-        	if(selectedCrystal != null) { crystal[selectedCrystal.x][selectedCrystal.y].setSelected(false); }
+            if(selectedCrystal != null) { crystal[selectedCrystal.x][selectedCrystal.y].setSelected(false); }
             crystal[ix][iy].setSelected(true);
             selectedCrystal = new Point(ix, iy);
         }
@@ -760,8 +852,8 @@
      * </code> is <code>null</code>.
      */
     public void setSelectedCrystal(Point ixy) throws NullPointerException {
-    	if(crystal != null) { setSelectedCrystal(ixy.x, ixy.y); }
-    	else { throw new NullPointerException("Crystal ID must be defined."); }
+        if(crystal != null) { setSelectedCrystal(ixy.x, ixy.y); }
+        else { throw new NullPointerException("Crystal ID must be defined."); }
     }
     
     /**
@@ -771,9 +863,9 @@
      * is set to <code>null</code>.
      */
     public void setSelectedCrystalHighlight(Color c) throws IllegalArgumentException {
-    	// We do not allow null values for the selected crystal color.
-    	if(c == null) { throw new IllegalArgumentException("Crystal selection color can not be null."); }
-    	else { selectedColor = c; }
+        // We do not allow null values for the selected crystal color.
+        if(c == null) { throw new IllegalArgumentException("Crystal selection color can not be null."); }
+        else { selectedColor = c; }
     }
     
     /**
@@ -784,92 +876,15 @@
      * it should not.
      */
     public void setSelectionHighlighting(boolean state) {
-    	if(enabledSelection != state) {
-    		enabledSelection = state;
-    		if(selectedCrystal != null) { crystal[selectedCrystal.x][selectedCrystal.y].repaint(); }
-    	}
-    }
-    
-    public void setSize(Dimension d) { setSize(d.width, d.height); }
-    
-    public void setSize(int width, int height) {
-    	// Run the superclass method.
-        super.setSize(width, height);
-        
-        // Resize the scale panel.
-        scalePanel.setLocation(width - scaleWidth, 0);
-        scalePanel.setSize(scaleWidth, height);
-        
-        // Determine the width and heights of the calorimeter crystals.
-        if (scalePanel.isVisible()) { width = getWidth() - scaleWidth; }
-        else { width = getWidth(); }
-        
-        int boxWidth = width / xBoxes;
-        int widthRem = width % xBoxes;
-        int boxHeight = height / yBoxes;
-        int heightRem = height % yBoxes;
-        
-        // Set positioning and sizing variables.
-        int[] crystalRem = { widthRem, heightRem };
-        int curX = 0;
-        int curY = 0;
-        
-        // Loop over the rows of crystals.
-        xPosition[0] = 0;
-        for(int x = 0; x < xBoxes; x++) {
-        	// Get the appropriate width for a crystal.
-        	int crystalWidth = boxWidth;
-        	if(crystalRem[0] != 0) {
-        		crystalWidth++;
-        		crystalRem[0]--;
-        	}
-        	
-        	// Note the x-coordinate for this column.
-        	xPosition[x + 1] = xPosition[x] + crystalWidth;
-        	
-        	// Loop over the columns of crystals.
-        	for(int y = 0; y < yBoxes; y++) {
-        		// Get the appropriate height for a crystal.
-        		int crystalHeight = boxHeight;
-        		if(crystalRem[1] != 0) {
-        			crystalHeight++;
-        			crystalRem[1]--;
-        		}
-        		
-        		// Note the y-coordinate for this row.
-        		yPosition[y + 1] = yPosition[y] + crystalHeight;
-        		
-        		// Set the crystal size and location.
-        		crystal[x][y].setSize(crystalWidth, crystalHeight);
-        		crystal[x][y].setLocation(curX, curY);
-        		
-        		// Increment the current y-coordinate.
-        		curY += crystalHeight;
-        	}
-        	
-        	// Increment the current x-coordinate and reset the current y-coordinate.
-        	curX += crystalWidth;
-        	curY = 0;
-        	
-        	// Reset the height remainder for the next column.
-        	crystalRem[1] = heightRem;
-        }
-            
-        // Calculate the cluster position variables.
-        double ltw = 0.25 * boxWidth;
-        double lth = 0.25 * boxHeight;
-        
-        if(ltw > lth) {
-        	clusterSpace[0] = (int)Math.round((boxWidth - lth - lth) / 2.0);
-        	clusterSpace[1] = (int)Math.round(lth);
-        	clusterSpace[2] = (int)Math.round(lth + lth);
-        }
-        else {
-        	clusterSpace[0] = (int)Math.round(ltw);
-        	clusterSpace[1] = (int)Math.round((boxHeight - ltw - ltw) / 2.0);
-        	clusterSpace[2] = (int)Math.round(ltw + ltw);
-        }
-	}
+        if(enabledSelection != state) {
+            // Process the change.
+            enabledSelection = state;
+            if(selectedCrystal != null) { crystal[selectedCrystal.x][selectedCrystal.y].repaint(); }
+            
+            // Throw an event.
+            throwSettingsEvent(SettingsEvent.PROPERTY_HOVER_HIGHLIGHT);
+        }
+    }
     
     /**
      * Sets whether the panel crystals should repaint automatically
@@ -894,20 +909,20 @@
      * @return Returns the appropriately mirrored point.
      */
     private Point getMirroredPoint(Point p, boolean flipX, boolean flipY) {
-    	// Handle the null case.
-    	if(p == null) { return null; }
-    	
-    	// Flip both x and y.
-    	if(flipX && flipY) { return new Point(getMirroredX(p.x), getMirroredY(p.y)); }
-    	
-    	// Flip only x.
-    	else if(flipX && !flipY) { return new Point(getMirroredX(p.x), p.y); } 
-    	
-    	// Flip only y.
-    	else if(!flipX && flipY) { return new Point(p.x, getMirroredY(p.y)); } 
-    	
-    	// Don't flip anything.
-    	else { return p; }
+        // Handle the null case.
+        if(p == null) { return null; }
+        
+        // Flip both x and y.
+        if(flipX && flipY) { return new Point(getMirroredX(p.x), getMirroredY(p.y)); }
+        
+        // Flip only x.
+        else if(flipX && !flipY) { return new Point(getMirroredX(p.x), p.y); } 
+        
+        // Flip only y.
+        else if(!flipX && flipY) { return new Point(p.x, getMirroredY(p.y)); } 
+        
+        // Don't flip anything.
+        else { return p; }
     }
     
     /**
@@ -926,10 +941,110 @@
      * Forces all crystals to revalidate their colors.
      */
     private void resetCrystalColors() {
-    	// Force all the crystals to update their colors.
-    	for(int ix = 0; ix < xBoxes; ix++) {
-    		for(int iy = 0; iy < yBoxes; iy++) { crystal[ix][iy].resetColor(); }
-    	}
+        // Force all the crystals to update their colors.
+        for(int ix = 0; ix < xBoxes; ix++) {
+            for(int iy = 0; iy < yBoxes; iy++) { crystal[ix][iy].resetColor(); }
+        }
+    }
+    
+    /**
+     * Sizes and positions all of the components on the panel.
+     */
+    private void resize() {
+        // Get the width and height.
+        int width = getWidth();
+        int height = getHeight();
+        
+        // Resize the scale panel.
+        scalePanel.setLocation(width - scaleWidth, 0);
+        scalePanel.setSize(scaleWidth, height);
+        
+        // Determine the width and heights of the calorimeter crystals.
+        if (scalePanel.isVisible()) { width = getWidth() - scaleWidth; }
+        else { width = getWidth(); }
+        
+        int boxWidth = width / xBoxes;
+        int widthRem = width % xBoxes;
+        int boxHeight = height / yBoxes;
+        int heightRem = height % yBoxes;
+        
+        // Set positioning and sizing variables.
+        int[] crystalRem = { widthRem, heightRem };
+        int curX = 0;
+        int curY = 0;
+        
+        // Loop over the rows of crystals.
+        xPosition[0] = 0;
+        for(int x = 0; x < xBoxes; x++) {
+            // Get the appropriate width for a crystal.
+            int crystalWidth = boxWidth;
+            if(crystalRem[0] != 0) {
+                crystalWidth++;
+                crystalRem[0]--;
+            }
+            
+            // Note the x-coordinate for this column.
+            xPosition[x + 1] = xPosition[x] + crystalWidth;
+            
+            // Loop over the columns of crystals.
+            for(int y = 0; y < yBoxes; y++) {
+                // Get the appropriate height for a crystal.
+                int crystalHeight = boxHeight;
+                if(crystalRem[1] != 0) {
+                    crystalHeight++;
+                    crystalRem[1]--;
+                }
+                
+                // Note the y-coordinate for this row.
+                yPosition[y + 1] = yPosition[y] + crystalHeight;
+                
+                // Set the crystal size and location.
+                crystal[x][y].setSize(crystalWidth, crystalHeight);
+                crystal[x][y].setLocation(curX, curY);
+                
+                // Increment the current y-coordinate.
+                curY += crystalHeight;
+            }
+            
+            // Increment the current x-coordinate and reset the current y-coordinate.
+            curX += crystalWidth;
+            curY = 0;
+            
+            // Reset the height remainder for the next column.
+            crystalRem[1] = heightRem;
+        }
+            
+        // Calculate the cluster position variables.
+        double ltw = 0.25 * boxWidth;
+        double lth = 0.25 * boxHeight;
+        
+        if(ltw > lth) {
+            clusterSpace[0] = (int)Math.round((boxWidth - lth - lth) / 2.0);
+            clusterSpace[1] = (int)Math.round(lth);
+            clusterSpace[2] = (int)Math.round(lth + lth);
+        }
+        else {
+            clusterSpace[0] = (int)Math.round(ltw);
+            clusterSpace[1] = (int)Math.round((boxHeight - ltw - ltw) / 2.0);
+            clusterSpace[2] = (int)Math.round(ltw + ltw);
+        }
+        
+    }
+    
+    /**
+     * Creates an appropriate <code>SettingsEvent</code> and passes it
+     * to all attached <code>SettingsListener</code> objects.
+     * @param propertyCode - The <code>int</code> primitive representing
+     * the setting that has changed.
+     */
+    private void throwSettingsEvent(int propertyCode) {
+        // Create a settings event.
+        SettingsEvent evt = new SettingsEvent(this, propertyCode);
+        
+        // Iterate over each settings listener and throw the event.
+        for(SettingsListener lst : listenerList) {
+            lst.settingChanged(evt);
+        }
     }
     
     /**
@@ -941,12 +1056,12 @@
      * and <code>false</code> if they are not.
      */
     private boolean validateIndices(int ix, int iy) {
-    	boolean lowX = (ix > -1);
-    	boolean highX = (ix < xBoxes);
-    	boolean lowY = (iy > -1);
-    	boolean highY = (iy < yBoxes);
-    	
-    	return (lowX && highX && lowY && highY);
+        boolean lowX = (ix > -1);
+        boolean highX = (ix < xBoxes);
+        boolean lowY = (iy > -1);
+        boolean highY = (iy < yBoxes);
+        
+        return (lowX && highX && lowY && highY);
     }
     
     /**
@@ -967,293 +1082,295 @@
      * Class <code>Crystal</code> holds all pertinent information needed
      * to display a calorimeter crystal in the panel. It also handles
      * drawing itself.
+     * 
      * @author Kyle McCarty
      */
     private class Crystal extends JPanel {
-		private static final long serialVersionUID = -5666423016127997831L;
-		// The energy stored in the crystal.
-		private double energy = 0.0;
-		// Whether the crystal can store energy.
-		private boolean disabled = false;
-		// Whether the crystal is a cluster center.
-		private boolean cluster = false;
-		// Whether zero-energy crystals should use color mapping.
-		private boolean useDefaultColor = false;
-		// Whether the crystal is selected.
-		private boolean selected = false;
-		// What color the crystal should be highlighted in.
-		private Color highlight = null;
-		// Store associated crystals.
-		private ArrayList<Association> componentList = new ArrayList<Association>();
-		
-		/**
-		 * Initializes a new calorimeter crystal panel.
-		 */
-    	public Crystal() {
-    		setOpaque(true);
-    		resetColor();
-    	}
-		
-		/**
-		 * Adds a new associated crystal to this crystal.
-		 * @param a - The <code>Association</code> object representing
-		 * the associated crystal and its highlighting color.
-		 */
-		public void addAssociation(Association a) {
-			// Add the association.
-			componentList.add(a);
-			
-			// If this crystal is selected, then activate the new crystal.
-			if(selected) { setCrystalHighlight(a.getChildCrystal(), a.getHighlight()); }
-		}
-    	
-    	/**
-		 * Increments the crystal's energy by the given amount.
-    	 * @param energy - The energy by which the crystal's stored
-    	 * energy should be increased.
-    	 */
-    	public void addEnergy(double energy) { setEnergy(this.energy + energy); }
-    	
-		/**
-		 * Clears all the associated crystal from this crystal.
-		 */
-		public void clearAssociations() {
-			// Remove the highlighting from any associated crystals,
-			// if this crystal is selected.
-			if(selected) {
-	    		for(Association a : componentList) { setCrystalHighlight(a.getChildCrystal(), null); }
-			}
-			
-			// Clear the list.
-			componentList.clear();
-		}
-    	
-    	/**
-		 * Indicates how much energy is stored in the crystal.
-    	 * @return Returns the crystal's energy as a <code>double</code>.
-    	 */
-    	public double getEnergy() { return energy; }
-    	
-    	/**
-		 * Indicates whether this crystal is also a cluster center.
-    	 * @return Returns <code>true</code> if the crystal is a cluster
-    	 * center and <code>false</code> if it is not.
-    	 */
-    	public boolean isClusterCenter() { return cluster; }
-    	
-    	/**
-		 * Indicates whether the crystal is disabled.
-    	 * @return Returns <code>true</code> if the crystal is disabled
-    	 * and <code>false</code> if it not.
-    	 */
-    	public boolean isDisabled() { return disabled; }
-    	
-    	public void paint(Graphics g) {
-    		// If the crystal's redraw is suppressed, do nothing.
-    		if(suppress) { return; }
-    		
-    		// Run the superclass paint method to draw the background.
-    		super.paint(g);
-    		
-    		// Draw the crystal border.
-    		g.setColor(Color.BLACK);
-    		g.drawRect(0, 0, getWidth(), getHeight());
-    		g.drawRect(1, 1, getWidth() - 2, getHeight() - 2);
-    		
-    		// If the crystal is not disabled, we may also add a
-    		// highlight to the crystal.
-    		if(!disabled) {
-    			// Highlighting from a crystal being selected overrides
-    			// any other form of highlighting.
-    			if(selected && enabledSelection) {
-        			g.setColor(selectedColor);
-        			g.drawRect(2, 2, getWidth() - 4, getHeight() - 4);
-    			}
-    			// Otherwise, if the crystal has a highlighting color
-    			// set, apply that color for the highlight.
-    			else if(getHighlight() != null) {
-        			g.setColor(getHighlight());
-        			g.drawRect(2, 2, getWidth() - 4, getHeight() - 4);
-        		}
-    		}
-    		
-    		// If the crystal contains a cluster, draw the cluster dot.
-    		if(cluster) {
-    			g.setColor(clusterColor);
-    			g.fillOval(clusterSpace[0], clusterSpace[1], clusterSpace[2], clusterSpace[2]);
-    		}
-    	}
-    	
-    	/**
-		 * Sets the crystals color to the appropriate value based on
-		 * its settings.
-    	 */
-    	public void resetColor() {
-    		// If the crystal is disabled, it is always disabledColor.
-    		if(disabled) { setBackground(disabledColor); }
-    		
-    		// If the crystal has zero energy and we are using a default
-    		// color, then we use that.
-    		else if(energy == 0.0 && useDefaultColor) { setBackground(defaultColor); }
-    		
-    		// Otherwise, we use the color chosen by the scale.
-    		else { setBackground(scale.getColor(energy)); }
-    	}
-		
-		/**
-		 * Sets whether the highlighting on this crystal's associated
-		 * crystals should be active or not.
-		 * @param state - <code>true</code> indicates that the crystal
-		 * highlighting should be active and <code>false</code> that
-		 * it should be inactive.
-		 */
-		public void setAssociatedActive(boolean state) {
-    		// If it has any associated crystals, either activate
-    		// or deactivate their highlighting, as per the current
-    		// crystal selection state.
-    		for(Association a : componentList) {
-    			if(state) { setCrystalHighlight(a.getChildCrystal(), a.getHighlight()); }
-    			else { setCrystalHighlight(a.getChildCrystal(), null); }
-    		}
-		}
-    	
-    	/**
-		 * Sets whether or not this crystal is a cluster center.
-    	 * @param state - <code>true</code> indicates that this is a
-    	 * cluster center and <code>false</code> that it is not.
-    	 */
-    	public void setClusterCenter(boolean state) {
-    		if(cluster != state) {
-    			cluster = state;
-    			repaint();
-    		}
-    	}
-    	
-    	/**
-		 * Sets whether or not this crystal can store energy.
-    	 * @param state - <code>true</code> means the crystal can not
-    	 * store energy and <code>false</code> that it can.
-    	 */
-    	public void setDisabled(boolean state) {
-    		// If the state has changed, reset the color.
-    		if(disabled != state) {
-    			disabled = state;
-    			resetColor();
-    		}
-    	}
-    	
-    	/**
-		 * Sets the crystal's stored energy.
-    	 * @param energy - The energy stored in the crystal.
-    	 */
-    	public void setEnergy(double energy) {
-    		if(this.energy != energy) {
-    			this.energy = energy;
-    			if(energy > extremum[1]) { extremum[1] = energy; }
-    			if(energy < extremum[0]) { extremum[0] = energy; }
-    			resetColor();
-    		}
-    	}
-    	
-    	/**
-		 * Sets what color the crystal should be highlighted with.
-		 * Note that selected crystals will always be highlighted with
-		 * the selected crystal color, though selecting a crystal does
-		 * not overwrite its highlight color.
-    	 * @param highlight - The color the crystal should be
-		 * highlighted in.
-    	 */
-    	public void setHighlight(Color highlight) {
-    		if(this.highlight != highlight) {
-    			this.highlight = highlight;
-    			repaint();
-    		}
-    	}
-    	
-    	/**
-		 * Sets whether or not this crystal should be highlighted as
-		 * a selected crystal.
-    	 * @param state - <code>true</code> means the crystal will be
-    	 * highlighted using the selected color and <code>false</code>
-    	 * will cause it to use the standard highlighting rules.
-    	 */
-    	public void setSelected(boolean state) {
-    		if(selected != state) {
-    			// Store the crystal's state and redraw it.
-        		selected = state;
-        		this.repaint();
-        		
-        		// Activate or deactivate the associated crystals.
-        		setAssociatedActive(state);
-    		}
-    	}
-    	
-    	/**
-		 * Sets the crystal's energy, cluster center status, and
-		 * highlighting color. The crystal will redraw itself if needed.
-    	 * @param energy - The crystal's energy.
-    	 * @param cluster - <code>true</code> indicates that the crystal
-    	 * is a cluster center, and <code>false</code> that is not.
-    	 * @param highlight - What color in which the crystal should be
-    	 * highlighted. <code>null</code> indicates that the crystal
-    	 * should not be highlighted.
-    	 */
-    	public void setState(double energy, boolean cluster, Color highlight) {
-    		// Track whether the crystal has changed states.
-    		boolean changed = false;
-    		
-    		// Change the energy if needed.
-    		if(this.energy != energy) {
-    			this.energy = energy;
-    			resetColor();
-    		}
-    		
-    		// Change the cluster state if needed.
-    		if(this.cluster != cluster) {
-    			this.cluster = cluster;
-    			changed = true;
-    		}
-    		
-    		// Change the highlighting if needed.
-    		if(this.highlight != highlight) {
-    			this.highlight = highlight;
-    			changed = true;
-    		}
-    		
-    		// If the state has changed, redraw the crystal.
-    		if(changed) { repaint(); }
-    	}
-    	
-    	/**
-		 * Sets whether the crystal should use a default color when it
-		 * has no energy.
-    	 * @param state - <code>true</code> means the crystal will render
-    	 * using a default color when it has no energy and <code>false
-    	 * </code> that it will use the scale mapping color at all times.
-    	 * @param autoRepaint
-    	 */
-    	public void setUseDefaultColor(boolean state, boolean autoRepaint) {
-    		// If the state has changed, reset the color.
-    		if(state != useDefaultColor) {
-    			useDefaultColor = state;
-    			if(autoRepaint) { resetColor(); }
-    		}
-    	}
-    	
-    	/**
-		 * Gets the highlight color assigned to this crystal.
-    	 * @return Returns the highlight color as a <code>Color</code>
-    	 * object if it exists. Otherwise, returns <code>null</code>.
-    	 */
-    	public Color getHighlight() { return highlight; }
+        private static final long serialVersionUID = -5666423016127997831L;
+        // The energy stored in the crystal.
+        private double energy = 0.0;
+        // Whether the crystal can store energy.
+        private boolean disabled = false;
+        // Whether the crystal is a cluster center.
+        private boolean cluster = false;
+        // Whether zero-energy crystals should use color mapping.
+        private boolean useDefaultColor = false;
+        // Whether the crystal is selected.
+        private boolean selected = false;
+        // What color the crystal should be highlighted in.
+        private Color highlight = null;
+        // Store associated crystals.
+        private ArrayList<Association> componentList = new ArrayList<Association>();
+        
+        /**
+         * Initializes a new calorimeter crystal panel.
+         */
+        public Crystal() {
+            setOpaque(true);
+            resetColor();
+        }
+        
+        /**
+         * Adds a new associated crystal to this crystal.
+         * @param a - The <code>Association</code> object representing
+         * the associated crystal and its highlighting color.
+         */
+        public void addAssociation(Association a) {
+            // Add the association.
+            componentList.add(a);
+            
+            // If this crystal is selected, then activate the new crystal.
+            if(selected) { setCrystalHighlight(a.getChildCrystal(), a.getHighlight()); }
+        }
+        
+        /**
+         * Increments the crystal's energy by the given amount.
+         * @param energy - The energy by which the crystal's stored
+         * energy should be increased.
+         */
+        public void addEnergy(double energy) { setEnergy(this.energy + energy); }
+        
+        /**
+         * Clears all the associated crystal from this crystal.
+         */
+        public void clearAssociations() {
+            // Remove the highlighting from any associated crystals,
+            // if this crystal is selected.
+            if(selected) {
+                for(Association a : componentList) { setCrystalHighlight(a.getChildCrystal(), null); }
+            }
+            
+            // Clear the list.
+            componentList.clear();
+        }
+        
+        /**
+         * Indicates how much energy is stored in the crystal.
+         * @return Returns the crystal's energy as a <code>double</code>.
+         */
+        public double getEnergy() { return energy; }
+        
+        /**
+         * Indicates whether this crystal is also a cluster center.
+         * @return Returns <code>true</code> if the crystal is a cluster
+         * center and <code>false</code> if it is not.
+         */
+        public boolean isClusterCenter() { return cluster; }
+        
+        /**
+         * Indicates whether the crystal is disabled.
+         * @return Returns <code>true</code> if the crystal is disabled
+         * and <code>false</code> if it not.
+         */
+        public boolean isDisabled() { return disabled; }
+        
+        @Override
+        public void paint(Graphics g) {
+            // If the crystal's redraw is suppressed, do nothing.
+            if(suppress) { return; }
+            
+            // Run the superclass paint method to draw the background.
+            super.paint(g);
+            
+            // Draw the crystal border.
+            g.setColor(Color.BLACK);
+            g.drawRect(0, 0, getWidth(), getHeight());
+            g.drawRect(1, 1, getWidth() - 2, getHeight() - 2);
+            
+            // If the crystal is not disabled, we may also add a
+            // highlight to the crystal.
+            if(!disabled) {
+                // Highlighting from a crystal being selected overrides
+                // any other form of highlighting.
+                if(selected && enabledSelection) {
+                    g.setColor(selectedColor);
+                    g.drawRect(2, 2, getWidth() - 4, getHeight() - 4);
+                }
+                // Otherwise, if the crystal has a highlighting color
+                // set, apply that color for the highlight.
+                else if(getHighlight() != null) {
+                    g.setColor(getHighlight());
+                    g.drawRect(2, 2, getWidth() - 4, getHeight() - 4);
+                }
+            }
+            
+            // If the crystal contains a cluster, draw the cluster dot.
+            if(cluster) {
+                g.setColor(clusterColor);
+                g.fillOval(clusterSpace[0], clusterSpace[1], clusterSpace[2], clusterSpace[2]);
+            }
+        }
+        
+        /**
+         * Sets the crystals color to the appropriate value based on
+         * its settings.
+         */
+        public void resetColor() {
+            // If the crystal is disabled, it is always disabledColor.
+            if(disabled) { setBackground(disabledColor); }
+            
+            // If the crystal has zero energy and we are using a default
+            // color, then we use that.
+            else if(energy == 0.0 && useDefaultColor) { setBackground(defaultColor); }
+            
+            // Otherwise, we use the color chosen by the scale.
+            else { setBackground(scale.getColor(energy)); }
+        }
+        
+        /**
+         * Sets whether the highlighting on this crystal's associated
+         * crystals should be active or not.
+         * @param state - <code>true</code> indicates that the crystal
+         * highlighting should be active and <code>false</code> that
+         * it should be inactive.
+         */
+        public void setAssociatedActive(boolean state) {
+            // If it has any associated crystals, either activate
+            // or deactivate their highlighting, as per the current
+            // crystal selection state.
+            for(Association a : componentList) {
+                if(state) { setCrystalHighlight(a.getChildCrystal(), a.getHighlight()); }
+                else { setCrystalHighlight(a.getChildCrystal(), null); }
+            }
+        }
+        
+        /**
+         * Sets whether or not this crystal is a cluster center.
+         * @param state - <code>true</code> indicates that this is a
+         * cluster center and <code>false</code> that it is not.
+         */
+        public void setClusterCenter(boolean state) {
+            if(cluster != state) {
+                cluster = state;
+                repaint();
+            }
+        }
+        
+        /**
+         * Sets whether or not this crystal can store energy.
+         * @param state - <code>true</code> means the crystal can not
+         * store energy and <code>false</code> that it can.
+         */
+        public void setDisabled(boolean state) {
+            // If the state has changed, reset the color.
+            if(disabled != state) {
+                disabled = state;
+                resetColor();
+            }
+        }
+        
+        /**
+         * Sets the crystal's stored energy.
+         * @param energy - The energy stored in the crystal.
+         */
+        public void setEnergy(double energy) {
+            if(this.energy != energy) {
+                this.energy = energy;
+                if(energy > extremum[1]) { extremum[1] = energy; }
+                if(energy < extremum[0]) { extremum[0] = energy; }
+                resetColor();
+            }
+        }
+        
+        /**
+         * Sets what color the crystal should be highlighted with.
+         * Note that selected crystals will always be highlighted with
+         * the selected crystal color, though selecting a crystal does
+         * not overwrite its highlight color.
+         * @param highlight - The color the crystal should be
+         * highlighted in.
+         */
+        public void setHighlight(Color highlight) {
+            if(this.highlight != highlight) {
+                this.highlight = highlight;
+                repaint();
+            }
+        }
+        
+        /**
+         * Sets whether or not this crystal should be highlighted as
+         * a selected crystal.
+         * @param state - <code>true</code> means the crystal will be
+         * highlighted using the selected color and <code>false</code>
+         * will cause it to use the standard highlighting rules.
+         */
+        public void setSelected(boolean state) {
+            if(selected != state) {
+                // Store the crystal's state and redraw it.
+                selected = state;
+                this.repaint();
+                
+                // Activate or deactivate the associated crystals.
+                setAssociatedActive(state);
+            }
+        }
+        
+        /**
+         * Sets the crystal's energy, cluster center status, and
+         * highlighting color. The crystal will redraw itself if needed.
+         * @param energy - The crystal's energy.
+         * @param cluster - <code>true</code> indicates that the crystal
+         * is a cluster center, and <code>false</code> that is not.
+         * @param highlight - What color in which the crystal should be
+         * highlighted. <code>null</code> indicates that the crystal
+         * should not be highlighted.
+         */
+        public void setState(double energy, boolean cluster, Color highlight) {
+            // Track whether the crystal has changed states.
+            boolean changed = false;
+            
+            // Change the energy if needed.
+            if(this.energy != energy) {
+                this.energy = energy;
+                resetColor();
+            }
+            
+            // Change the cluster state if needed.
+            if(this.cluster != cluster) {
+                this.cluster = cluster;
+                changed = true;
+            }
+            
+            // Change the highlighting if needed.
+            if(this.highlight != highlight) {
+                this.highlight = highlight;
+                changed = true;
+            }
+            
+            // If the state has changed, redraw the crystal.
+            if(changed) { repaint(); }
+        }
+        
+        /**
+         * Sets whether the crystal should use a default color when it
+         * has no energy.
+         * @param state - <code>true</code> means the crystal will render
+         * using a default color when it has no energy and <code>false
+         * </code> that it will use the scale mapping color at all times.
+         * @param autoRepaint
+         */
+        public void setUseDefaultColor(boolean state, boolean autoRepaint) {
+            // If the state has changed, reset the color.
+            if(state != useDefaultColor) {
+                useDefaultColor = state;
+                if(autoRepaint) { resetColor(); }
+            }
+        }
+        
+        /**
+         * Gets the highlight color assigned to this crystal.
+         * @return Returns the highlight color as a <code>Color</code>
+         * object if it exists. Otherwise, returns <code>null</code>.
+         */
+        public Color getHighlight() { return highlight; }
     }
     
     /**
      * The local class <b>ScalePanel</b> renders the scale for the calorimeter
      * color map.
-     **/
+     */
     private class ScalePanel extends JPanel {
-		private static final long serialVersionUID = -2644562244208528609L;
+        private static final long serialVersionUID = -2644562244208528609L;
         
         protected void paintComponent(Graphics g) {
             // Set the text region width.
@@ -1366,4 +1483,4 @@
             }
         }
     }
-}
+}

Modified: java/trunk/ecal-event-display/src/main/java/org/hps/monitoring/ecal/eventdisplay/ui/CrystalFilterPanel.java
 =============================================================================
--- java/trunk/ecal-event-display/src/main/java/org/hps/monitoring/ecal/eventdisplay/ui/CrystalFilterPanel.java	(original)
+++ java/trunk/ecal-event-display/src/main/java/org/hps/monitoring/ecal/eventdisplay/ui/CrystalFilterPanel.java	Tue Nov  4 10:19:31 2014
@@ -253,6 +253,9 @@
                 // Remove the list of filtered crystals.
                 removeFilter();
                 
+                // Disable the remove filter button.
+                buttonRemove.setEnabled(false);
+                
                 // Disable all of the filter check boxes.
                 for(JCheckBox check : checkActive) {
                     check.setSelected(false);

Modified: java/trunk/ecal-event-display/src/main/java/org/hps/monitoring/ecal/eventdisplay/ui/DataFileViewer.java
 =============================================================================
--- java/trunk/ecal-event-display/src/main/java/org/hps/monitoring/ecal/eventdisplay/ui/DataFileViewer.java	(original)
+++ java/trunk/ecal-event-display/src/main/java/org/hps/monitoring/ecal/eventdisplay/ui/DataFileViewer.java	Tue Nov  4 10:19:31 2014
@@ -3,14 +3,17 @@
 import java.awt.Point;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
-import java.awt.event.KeyAdapter;
 import java.awt.event.KeyEvent;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.io.File;
 import java.io.IOException;
 import java.util.List;
 
 import javax.swing.JFrame;
+import javax.swing.JMenuItem;
+import javax.swing.KeyStroke;
 
-import org.hps.monitoring.ecal.eventdisplay.io.EventManager;
 import org.hps.monitoring.ecal.eventdisplay.util.CrystalDataSet;
 import org.hps.monitoring.ecal.eventdisplay.util.EcalWiringManager;
 
@@ -61,7 +64,7 @@
      * @throws IOException Occurs if there is an error reading from
      * either data source.
      */
-    public DataFileViewer(EventManager dataSource, String crystalDataFilePath) throws IOException {
+    public DataFileViewer(File dataSource, String crystalDataFilePath) throws IOException {
         // Initialize the super class file.
         super(dataSource);
         
@@ -69,9 +72,7 @@
         ewm = new EcalWiringManager(crystalDataFilePath);
         
         // Add the crystal data fields.
-        for(String fieldName : fieldNames) {
-            addStatusField(fieldName);
-        }
+        for(String fieldName : fieldNames) { addStatusField(fieldName); }
         
         // Instantiate the crystal filter panel.
         filterPanel = new CrystalFilterPanel(ewm);
@@ -81,38 +82,42 @@
         filterWindow.pack();
         filterWindow.setResizable(false);
         
+        // Add a new view menu option to display the filter panel.
+        JMenuItem menuFilter = new JMenuItem("Show Filter", KeyEvent.VK_F);
+        menuFilter.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F, ActionEvent.CTRL_MASK));
+        menuFilter.addActionListener(new ActionListener() {
+            @Override
+            public void actionPerformed(ActionEvent e) { filterWindow.setVisible(true); }
+        });
+        menu[MENU_VIEW].addSeparator();
+        menu[MENU_VIEW].add(menuFilter);
+        
         // Add an action listener to note when the filter window applies
         // a crystal filter.
         filterPanel.addActionListener(new ActionListener() {
-			@Override
-			public void actionPerformed(ActionEvent e) {
-				// Clear the panel highlighting.
-				ecalPanel.clearHighlight();
-				
-				// If the filter panel is active, highlight the crystals
-				// that passed the filter.
-				if(filterPanel.isActive()) {
-					// Get the list of filtered crystals.
-					List<Point> filterList = filterPanel.getFilteredCrystals();
-					
-					// Highlight each of the filtered crystals.
-					for(Point crystal : filterList) {
-						ecalPanel.setCrystalHighlight(toPanelPoint(crystal), java.awt.Color.WHITE);
-					}
-				}
-			}
+            @Override
+            public void actionPerformed(ActionEvent e) {
+                // Clear the panel highlighting.
+                ecalPanel.clearHighlight();
+                
+                // If the filter panel is active, highlight the crystals
+                // that passed the filter.
+                if(filterPanel.isActive()) {
+                    // Get the list of filtered crystals.
+                    List<Point> filterList = filterPanel.getFilteredCrystals();
+                    
+                    // Highlight each of the filtered crystals.
+                    for(Point crystal : filterList) {
+                        ecalPanel.setCrystalHighlight(toPanelPoint(crystal), java.awt.Color.WHITE);
+                    }
+                }
+            }
         });
         
-        // Create a new key listener.
-        addKeyListener(new KeyAdapter() {
-			@Override
-			public void keyReleased(KeyEvent e) {
-				// Bring up the filter panel when 'f' is pressed.
-				if(e.getKeyCode() == 70) {
-					if(!filterWindow.isVisible()) { filterWindow.setVisible(true); }
-					else { filterWindow.setVisible(false); }
-				}
-			}
+        // Kill the filter window on system close.
+        addWindowListener(new WindowAdapter() {
+            @Override
+            public void windowClosed(WindowEvent e) { filterWindow.dispose(); }
         });
     }
     

Modified: java/trunk/ecal-event-display/src/main/java/org/hps/monitoring/ecal/eventdisplay/ui/FileViewer.java
 =============================================================================
--- java/trunk/ecal-event-display/src/main/java/org/hps/monitoring/ecal/eventdisplay/ui/FileViewer.java	(original)
+++ java/trunk/ecal-event-display/src/main/java/org/hps/monitoring/ecal/eventdisplay/ui/FileViewer.java	Tue Nov  4 10:19:31 2014
@@ -1,12 +1,23 @@
 package org.hps.monitoring.ecal.eventdisplay.ui;
 
 import org.hps.monitoring.ecal.eventdisplay.io.EventManager;
+import org.hps.monitoring.ecal.eventdisplay.io.LCIOManager;
+import org.hps.monitoring.ecal.eventdisplay.io.TextManager;
 
 import java.awt.Point;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.KeyAdapter;
+import java.awt.event.KeyEvent;
+import java.io.File;
 import java.io.IOException;
 import java.text.DecimalFormat;
 import java.util.HashMap;
 import java.util.List;
+
+import javax.swing.JFileChooser;
+import javax.swing.JMenuItem;
+import javax.swing.filechooser.FileNameExtensionFilter;
 
 import org.hps.monitoring.ecal.eventdisplay.event.Association;
 import org.hps.monitoring.ecal.eventdisplay.event.Cluster;
@@ -20,12 +31,21 @@
  * 
  * @author Kyle McCarty
  */
-public class FileViewer extends ActiveViewer {
+public class FileViewer extends Viewer {
     private static final long serialVersionUID = 17058336873349781L;
+    
+    // Gets events from some file.
+    protected EventManager em = null;
+    
+    // File chooser for opening new files.
+    private JFileChooser fileChooser;
+    
     // Map cluster location to a cluster object.
     private HashMap<Point, Cluster> clusterMap = new HashMap<Point, Cluster>();
+    
     // Additional status display field names for this data type.
     private static final String[] fieldNames = { "Event Number", "Shared Hits", "Component Hits", "Cluster Energy" };
+    
     // Indices for the field values.
     private static final int EVENT_NUMBER = 0;
     private static final int SHARED_HITS = 1;
@@ -33,8 +53,6 @@
     private static final int CLUSTER_ENERGY = 3;
     
     /**
-     * <b>FileViewer</b><br/><br/>
-     * <code>public <b>FileViewer</b>()</code><br/><br/>
      * Constructs a new <code>Viewer</code> for displaying data read
      * from a file.
      * @param dataSource - The <code>EventManager</code> responsible
@@ -42,21 +60,71 @@
      * @throws NullPointerException Occurs if the event manager is
      * <code>null</code>.
      */
-    public FileViewer(EventManager dataSource) throws NullPointerException {
-        // Initialize the superclass viewer.
-        super(dataSource);
+    public FileViewer(File dataSource) throws NullPointerException {
+        // Initialize the superclass.
+        super();
+        
+        // Make a key listener to change events.
+        addKeyListener(new EcalKeyListener());
         
         // Add additional fields.
         insertStatusField(0, fieldNames[0]);
         for(int index = 1; index < fieldNames.length; index++) {
             addStatusField(fieldNames[index]);
         }
-    }
-    
-    @Override
+        
+        // Initialize the file chooser.
+        fileChooser = new JFileChooser(new File("D:\\cygwin64\\home\\Kyle\\background\\compiled\\output\\")); 
+        fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
+        fileChooser.addChoosableFileFilter(new FileNameExtensionFilter("Text Files", "txt"));
+        fileChooser.addChoosableFileFilter(new FileNameExtensionFilter("LCIO Files", "lcio", "slcio"));
+        
+        // Add an open file option to the file menu.
+        JMenuItem menuOpen = new JMenuItem("Open File", KeyEvent.VK_O);
+        menuOpen.addActionListener(new ActionListener() {
+            @Override
+            public void actionPerformed(ActionEvent e) {
+                // Open the file chooser and note what action was taken.
+                int result = fileChooser.showOpenDialog(FileViewer.this);
+                
+                // If a file was selected, load it.
+                if(result == JFileChooser.APPROVE_OPTION) {
+                    // Get the selected data file.
+                    File dataFile = fileChooser.getSelectedFile();
+                    
+                    // Load the indicated file.
+                    openDataSource(dataFile);
+                }
+            }
+        });
+        menu[MENU_FILE].addSeparator();
+        menu[MENU_FILE].add(menuOpen);
+        
+        // Add an exit command to the file menu.
+        JMenuItem menuExit = new JMenuItem("Exit", KeyEvent.VK_X);
+        menuExit.addActionListener(new ActionListener() {
+            @Override
+            public void actionPerformed(ActionEvent e) { dispose(); }
+        });
+        menu[MENU_FILE].add(menuExit);
+        
+        // Set the calorimeter panel to display a logarithmic scale.
+        setUseLogarithmicScale();
+        
+        // Set the data source.
+        if(dataSource != null) { openDataSource(dataSource); }
+    }
+    
+    /**
+     * Feeds the calorimeter panel the data from the next event.
+     * @throws IOException Occurs when there is an issue with reading the data file.
+     */
     public void displayNextEvent() throws IOException { getEvent(true); }
     
-    @Override
+    /**
+     * Feeds the calorimeter panel the data from the previous event.
+     * @throws IOException Occurs when there is an issue with reading the data file.
+     */
     public void displayPreviousEvent() throws IOException { getEvent(false); }
     
     @Override
@@ -98,12 +166,11 @@
         else { for(String field : fieldNames) { setStatusField(field, ResizableFieldPanel.NULL_VALUE); } }
         
         // Set the event number.
-        setStatusField(fieldNames[EVENT_NUMBER], Integer.toString(em.getEventNumber()));
-    }
-    
-    /**
-     * <b>displayEvent</b><br/><br/>
-     * <code>private void <b>displayEvent</b>(List<EcalHit> hitList, List<Cluster> clusterList)</code><br/><br/>
+        if(em != null) { setStatusField(fieldNames[EVENT_NUMBER], Integer.toString(em.getEventNumber())); }
+        else { setStatusField(fieldNames[EVENT_NUMBER], ResizableFieldPanel.NULL_VALUE); }
+    }
+    
+    /**
      * Displays the given lists of hits and clusters on the calorimeter
      * panel.
      * @param hitList - A list of hits for the current event.
@@ -146,8 +213,6 @@
     }
     
     /**
-     * <b>getEvent</b><br/><br/>
-     * <code>private void <b>getEvent</b>(boolean forward)</code><br/><br/>
      * Reads either the next or the previous event from the event manager.
      * @param forward - Whether the event data should be read forward
      * or backward.
@@ -171,4 +236,85 @@
         // Display it.
         displayEvent(em.getHits(), em.getClusters());
     }
-}
+    
+    /**
+     * Loads a data source for viewing, if it is a supported data type.
+     * @param dataFile - The file to load.
+     * @return Returns <code>true</code> if the file was successfully
+     * loaded and <code>false</code> if it was not.
+     */
+    private boolean openDataSource(File dataFile) {
+        // Get the file extension.
+        int point = dataFile.getName().lastIndexOf('.');
+        String extension = "";
+        if(point != -1 && point != dataFile.getName().length()) {
+            extension = dataFile.getName().substring(point + 1);
+        }
+        
+        // Open the file with a manager appropriate to it.
+        EventManager manager = null;
+        if(extension.compareToIgnoreCase("lcio") == 0 || extension.compareToIgnoreCase("slcio") == 0) {
+            // Try to open it.
+            try { manager = new LCIOManager(dataFile); }
+            catch(IOException exc) {
+                System.err.println("Error reading data file.");
+            }
+        }
+        else if(extension.compareToIgnoreCase("txt") == 0) {
+            try { manager = new TextManager(dataFile); }
+            catch(IOException exc) {
+                System.err.println("Error reading data file.");
+            }
+        }
+        else { System.err.println("Unrecognized file type."); }
+        
+        // If the file was successfully loaded, set it as
+        // the current data manager and display the first
+        // event.
+        if(manager != null) {
+            em = manager;
+            displayEvent(manager.getHits(), manager.getClusters());
+            setTitle("HPS Ecal Event Display - " + dataFile.getName());
+            displayEvent(em.getHits(), em.getClusters());
+            updateStatusPanel();
+            return true;
+        }
+        
+        // Otherwise, report that the file could not be opened.
+        setTitle("HPS Ecal Event Display");
+        updateStatusPanel();
+        return false;
+    }
+    
+    /**
+     * The <code>EcalListener</code> class binds keys to actions.
+     * Bound actions include:
+     * [Right Arrow] :: Next event
+     * [Left Arrow ] :: Previous event
+     **/
+    private class EcalKeyListener extends KeyAdapter {
+        @Override
+        public void keyReleased(KeyEvent e) {
+            // If right-arrow was pressed, go to the next event.
+            if (e.getKeyCode() == 39) {
+                try { displayNextEvent(); }
+                catch (IOException ex) {
+                    System.err.println(ex.getMessage());
+                    System.exit(1);
+                }
+            }
+            
+            // If left-arrow was pressed, go to the next event.
+            else if (e.getKeyCode() == 37) {
+                try { displayPreviousEvent(); }
+                catch (IOException ex) {
+                    System.err.println(ex.getMessage());
+                    System.exit(1);
+                }
+            }
+            
+            // Otherwise, print out the key code for the pressed key.
+            else { System.out.printf("Key Code: %d%n", e.getKeyCode()); }
+        }
+    }
+}

Modified: java/trunk/ecal-event-display/src/main/java/org/hps/monitoring/ecal/eventdisplay/ui/OccupancyViewer.java
 =============================================================================
--- java/trunk/ecal-event-display/src/main/java/org/hps/monitoring/ecal/eventdisplay/ui/OccupancyViewer.java	(original)
+++ java/trunk/ecal-event-display/src/main/java/org/hps/monitoring/ecal/eventdisplay/ui/OccupancyViewer.java	Tue Nov  4 10:19:31 2014
@@ -16,6 +16,7 @@
  * 
  * @author Kyle McCarty
  */
+@Deprecated
 public class OccupancyViewer extends ActiveViewer {
     private static final long serialVersionUID = 3712604287904215617L;
     // The number of events that have been read so far.

Modified: java/trunk/ecal-event-display/src/main/java/org/hps/monitoring/ecal/eventdisplay/ui/Viewer.java
 =============================================================================
--- java/trunk/ecal-event-display/src/main/java/org/hps/monitoring/ecal/eventdisplay/ui/Viewer.java	(original)
+++ java/trunk/ecal-event-display/src/main/java/org/hps/monitoring/ecal/eventdisplay/ui/Viewer.java	Tue Nov  4 10:19:31 2014
@@ -3,21 +3,37 @@
 import java.awt.Color;
 import java.awt.Dimension;
 import java.awt.Point;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.ComponentAdapter;
 import java.awt.event.ComponentEvent;
-import java.awt.event.ComponentListener;
+import java.awt.event.KeyEvent;
+import java.awt.event.MouseAdapter;
 import java.awt.event.MouseEvent;
-import java.awt.event.MouseListener;
-import java.awt.event.MouseMotionListener;
+import java.awt.event.MouseMotionAdapter;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.IOException;
 import java.text.DecimalFormat;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.NoSuchElementException;
 
+import javax.imageio.ImageIO;
+import javax.swing.JCheckBoxMenuItem;
 import javax.swing.JFrame;
+import javax.swing.JMenu;
+import javax.swing.JMenuBar;
+import javax.swing.JMenuItem;
+import javax.swing.JRadioButtonMenuItem;
+import javax.swing.JScrollPane;
+import javax.swing.KeyStroke;
 
 import org.hps.monitoring.ecal.eventdisplay.util.CrystalEvent;
 import org.hps.monitoring.ecal.eventdisplay.util.CrystalListener;
+import org.hps.monitoring.ecal.eventdisplay.util.SettingsEvent;
+import org.hps.monitoring.ecal.eventdisplay.util.SettingsListener;
 
 /**
  * The abstract class <code>Viewer</code> handles initialization of the
@@ -28,18 +44,54 @@
  * @author Kyle McCarty
  **/
 public abstract class Viewer extends JFrame {
-    // Java-suggested variable.
-    private static final long serialVersionUID = -2022819652687941812L;
+    // Serialization UID.
+    private static final long serialVersionUID = 2L;
+    
     // A map of field names to field indices.
     private Map<String, Integer> fieldMap = new HashMap<String, Integer>();
+    
     // A list of crystal listeners attached to the viewer.
     private ArrayList<CrystalListener> listenerList = new ArrayList<CrystalListener>();
+    
+    // Menus and menu items.
+    private final JMenuItem menuScreenshot;
+    private final JCheckBoxMenuItem menuHighlight;
+    private final JCheckBoxMenuItem menuBackground;
+    private final JCheckBoxMenuItem menuMirrorX;
+    private final JCheckBoxMenuItem menuMirrorY;
+    private final JRadioButtonMenuItem[] menuScaling;
+    
     // The default field names.
     private static final String[] defaultFields = { "x Index", "y Index", "Cell Value" };
+    
+    // The default crystal color.
+    private static final Color DEFAULT_CRYSTAL_COLOR = Color.GRAY;
+    
     // Indices for the field values.
     private static final int X_INDEX = 0;
     private static final int Y_INDEX = 1;
     private static final int CELL_VALUE = 2;
+    private static final int MENU_ITEM_LIN_SCALE = 0;
+    private static final int MENU_ITEM_LOG_SCALE = 1;
+    protected static final int MENU_FILE = 0;
+    protected static final int MENU_VIEW = 1;
+    protected static final int MENU_SCALE = 2;
+    
+    /**
+     * The root menu bar displayed by the <code>Viewer</code>.
+     */
+    protected final JMenuBar menuRoot;
+    
+    /**
+     * The base menus used displayed by the <code>Viewer</code>. 
+     */
+    protected final JMenu[] menu;
+    
+    /**
+     * Component that allows for scrolling functionality when there
+     * are more status panel entries then can be displayed at once.
+     */
+    protected final JScrollPane statusScroller;
     
     /**
      * The component responsible for displaying status information 
@@ -61,6 +113,11 @@
      * The default color for highlighting cluster shared hits.
      */
     public static final Color HIGHLIGHT_CLUSTER_SHARED = Color.YELLOW;
+    
+    /**
+     * The default color for generic crystal highlighting.
+     */
+    public static final Color HIGHLIGHT_GENERIC = Color.WHITE;
     
     /**
      * Initializes the viewer window and calorimeter panel.
@@ -70,12 +127,21 @@
      * arguments are <code>null</code>.
      **/
     public Viewer() throws NullPointerException {
+        // ==========================================================
+        // ==== Initialize Base Component Properties ================
+        // ==========================================================
+        
         // Initialize the underlying JPanel.
         super();
         
         // Generate the status panel.
         statusPanel = new ResizableFieldPanel(100);
         statusPanel.setBackground(Color.WHITE);
+        statusScroller = new JScrollPane(statusPanel);
+        statusScroller.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
+        statusScroller.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
+        statusScroller.getVerticalScrollBar().setBlockIncrement(5);
+        statusScroller.getVerticalScrollBar().setUnitIncrement(5);
         
         // Add the default fields.
         for(String field : defaultFields) { addStatusField(field); }
@@ -84,6 +150,7 @@
         ecalPanel.setScaleMinimum(0.001);
         ecalPanel.setScaleMaximum(3.0);
         ecalPanel.setScalingLinear();
+        ecalPanel.addSettingsListener(new PropertyUpdater());
         
         // Disable the crystals in the calorimeter panel along the beam gap.
         for (int i = -23; i < 24; i++) {
@@ -100,7 +167,7 @@
         
         // Add the panels.
         add(ecalPanel);
-        add(statusPanel);
+        add(statusScroller);
         
         // Define viewer panel properties.
         setTitle("HPS Ecal Event Display");
@@ -110,7 +177,123 @@
         setLayout(null);
         
         // Add a listener to update everything when the window changes size
-        addComponentListener(new ResizeListener());
+        addComponentListener(new ComponentAdapter() {
+            @Override
+            public void componentResized(ComponentEvent e) { resize(); }
+        });
+
+        // ==========================================================
+        // ==== Initialize Menu Properties ==========================
+        // ==========================================================
+        
+        // Create a menu bar to render panel options easily accessible.
+        menuRoot = new JMenuBar();
+        
+        // Create the base menu options.
+        int MENU_FILE = 0;
+        int MENU_VIEW = 1;
+        int MENU_SCALE = 2;
+        menu = new JMenu[3];
+        
+        // ==== Instantiate the File Menu ===========================
+        // ==========================================================
+        
+        // Define the file menu.
+        menu[MENU_FILE] = new JMenu("File");
+        menu[MENU_FILE].setMnemonic(KeyEvent.VK_F);
+        
+        // Define the screenshot menu item.
+        menuScreenshot = new JMenuItem("Save Screenshot", KeyEvent.VK_S);
+        menuScreenshot.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, ActionEvent.CTRL_MASK));
+        menuScreenshot.addActionListener(new ActionListener() {
+            @Override
+            public void actionPerformed(ActionEvent e) { saveScreenshot(); }
+        });
+        menu[MENU_FILE].add(menuScreenshot);
+        
+        // ==== Instantiate the View Menu ===========================
+        // ==========================================================
+        
+        // Define the view menu.
+        menu[MENU_VIEW] = new JMenu("View");
+        menu[MENU_VIEW].setMnemonic(KeyEvent.VK_V);
+        
+        // Define the background toggle menu item.
+        menuBackground = new JCheckBoxMenuItem("Zero-Energy Color Mapping", usesZeroEnergyColorMapping());
+        menuBackground.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_B, ActionEvent.CTRL_MASK));
+        menuBackground.setMnemonic(KeyEvent.VK_Z);
+        menuBackground.addActionListener(new ActionListener() {
+            @Override
+            public void actionPerformed(ActionEvent e) {
+                setUseZeroEnergyColorMapping(menuBackground.isSelected());
+            }
+        });
+        menu[MENU_VIEW].add(menuBackground);
+        
+        // Define the toggle highlighting menu item.
+        menuHighlight = new JCheckBoxMenuItem("Highlight Active Crystal", usesCrystalHighlighting());
+        menuHighlight.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_H, ActionEvent.CTRL_MASK));
+        menuHighlight.setMnemonic(KeyEvent.VK_H);
+        menuHighlight.addActionListener(new ActionListener() {
+            @Override
+            public void actionPerformed(ActionEvent e) {
+                setUseCrystalHighlighting(!usesCrystalHighlighting());
+            }
+        });
+        menu[MENU_VIEW].add(menuHighlight);
+        
+        // Define the mirror x-axis menu item.
+        menuMirrorX = new JCheckBoxMenuItem("Mirror x-Axis", isMirroredX());
+        menuMirrorX.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_X, ActionEvent.ALT_MASK));
+        menuMirrorX.setMnemonic(KeyEvent.VK_X);
+        menuMirrorX.addActionListener(new ActionListener() {
+            @Override
+            public void actionPerformed(ActionEvent e) { setMirrorX(!isMirroredX()); }
+        });
+        menu[MENU_VIEW].add(menuMirrorX);
+        
+        // Define the mirror y-axis menu item.
+        menuMirrorY = new JCheckBoxMenuItem("Mirror y-Axis", isMirroredY());
+        menuMirrorY.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Y, ActionEvent.ALT_MASK));
+        menuMirrorY.setMnemonic(KeyEvent.VK_Y);
+        menuMirrorY.addActionListener(new ActionListener() {
+            @Override
+            public void actionPerformed(ActionEvent e) { setMirrorY(!isMirroredY()); }
+        });
+        menu[MENU_VIEW].add(menuMirrorY);
+        
+        // ==== Instantiate the Scale Menu ==========================
+        // ==========================================================
+        
+        // Define the scale menu button.
+        menu[MENU_SCALE] = new JMenu("Scale");
+        menu[MENU_SCALE].setMnemonic(KeyEvent.VK_S);
+        
+        // Attach the menu to the panel.
+        setJMenuBar(menuRoot);
+        
+        // Define the linear/logarithmic menu items.
+        menuScaling = new JRadioButtonMenuItem[2];
+        menuScaling[MENU_ITEM_LIN_SCALE] = new JRadioButtonMenuItem("Linear Scale", usesLinearScale());
+        menuScaling[MENU_ITEM_LOG_SCALE] = new JRadioButtonMenuItem("Logarithmic Scale", usesLogarithmicScale());
+        menuScaling[MENU_ITEM_LIN_SCALE].setMnemonic(KeyEvent.VK_I);
+        menuScaling[MENU_ITEM_LOG_SCALE].setMnemonic(KeyEvent.VK_O);
+        menuScaling[MENU_ITEM_LIN_SCALE].addActionListener(new ActionListener() {
+            @Override
+            public void actionPerformed(ActionEvent e) { scaleChange(MENU_ITEM_LIN_SCALE); }
+        });
+        menuScaling[MENU_ITEM_LOG_SCALE].addActionListener(new ActionListener() {
+            @Override
+            public void actionPerformed(ActionEvent e) { scaleChange(MENU_ITEM_LOG_SCALE); }
+        });
+        menu[MENU_SCALE].add(menuScaling[MENU_ITEM_LIN_SCALE]);
+        menu[MENU_SCALE].add(menuScaling[MENU_ITEM_LOG_SCALE]);
+        
+        // ==== Add the Menus to the Root ===========================
+        // ==========================================================
+        
+        // Add the menu items to the root.
+        for(JMenuItem menuItem : menu) { menuRoot.add(menuItem); }
     }
     
     /**
@@ -120,30 +303,152 @@
      * no exception is thrown and no action is performed. 
      * @param cl - The listener to add.
      */
-    public void addCrystalListener(CrystalListener cl) {
+    public final void addCrystalListener(CrystalListener cl) {
         if(cl != null) { listenerList.add(cl); }
     }
     
     /**
-     * Adds a new field to the status panel.
-     * @param fieldName - The name to display for the field and that
-     * links to the field when calling <code>setStatusField</code>.
-     */
-    protected void addStatusField(String fieldName) {
-        fieldMap.put(fieldName, statusPanel.getFieldCount());
-        statusPanel.addField(fieldName);
-    }
-    
-    /**
-     * Inserts the field at the indicated location on the status panel.
-     * @param index - The index at which to insert the field.
-     * @param fieldName - The name to display for the field and that
-     * links to the field when calling <code>setStatusField</code>.
-     */
-    protected void insertStatusField(int index, String fieldName) {
-        statusPanel.insertField(index, fieldName);
-        fieldMap = statusPanel.getFieldNameIndexMap();
-    }
+     * Gets all of the crystal listeners attached to this object.
+     * @return Returns the crystal listeners as an array of <code>
+     * CrystalListener</code> objects.
+     */
+    public final CrystalListener[] getCrystalListeners() {
+        return listenerList.toArray(new CrystalListener[listenerList.size()]);
+    }
+    
+    /**
+     * Indicates whether the menu bar is visible or not.
+     * @return Returns <code>true</code> if the menu bar is visible
+     * and <code>false</code> otherwise.
+     */
+    public boolean isMenuVisible() {
+        return menuRoot.isVisible();
+    }
+    
+    /**
+     * Indicates whether the calorimeter panel displays the x-axis
+     * in mirrored orientation or not. Mirrored orientation displays
+     * positive LCSim coordinates on the left-hand side and negative
+     * coordinates on the right-hand side. Regular orientation goes
+     * from negative coordinates on the left-hand side to positive
+     * coordinates on the right-hand side.
+     * @return Returns <code>true</code> if the panel is using mirrored
+     * coordinates and <code>false</code> if the panel is using regular
+     * coordinates.
+     */
+    public boolean isMirroredX() {
+        return ecalPanel.isMirroredX();
+    }
+    
+    /**
+     * Indicates whether the calorimeter panel displays the y-axis
+     * in mirrored orientation or not. Mirrored orientation displays
+     * positive LCSim coordinates on the bottom side and negative
+     * coordinates on the top side. Regular orientation goes from
+     * negative coordinates on the bottom side to positive coordinates
+     * on the top side.
+     * @return Returns <code>true</code> if the panel is using mirrored
+     * coordinates and <code>false</code> if the panel is using regular
+     * coordinates.
+     */
+    public boolean isMirroredY() {
+        return ecalPanel.isMirroredY();
+    }
+    
+    /**
+     * Removes the specified crystal listener so that it no longer
+     * receives crystal events from this component. This method performs
+     * no function, nor does it throw an exception, if the listener
+     * specified by the argument was not previously added to this
+     * component. If listener <code>cl</code> is <code>null</code>, no
+     * exception is thrown and no action is performed. 
+     * @param cl - The listener to remove.
+     */
+    public final void removeCrystalListener(CrystalListener cl) {
+        if(cl != null) { listenerList.remove(cl); }
+    }
+    
+    /**
+     * Sets the menu bar to be either visible or hidden.
+     * @param isVisible - <code>true</code> indicates that the menu is
+     * visible and <code>false</code> that it is not.
+     */
+    public final void setMenuVisible(boolean isVisible) {
+        menuRoot.setVisible(isVisible);
+    }
+    
+    /**
+     * Sets whether to mirror the x-axis on the calorimeter display.
+     * @param state - <code>true</code> indicates that the axis should
+     * be mirrored and <code>false</code> that it should not.
+     */
+    public void setMirrorX(boolean state) {
+        ecalPanel.setMirrorX(state);
+        updateStatusPanel();
+    }
+    
+    /**
+     * Sets whether to mirror the y-axis on the calorimeter display.
+     * @param state - <code>true</code> indicates that the axis should
+     * be mirrored and <code>false</code> that it should not.
+     */
+    public void setMirrorY(boolean state) {
+        ecalPanel.setMirrorY(state);
+        updateStatusPanel();
+    }
+    
+    /**
+     * Sets the value of the indicated status field on the calorimeter
+     * display.
+     * @param fieldName - The name of the field to set.
+     * @param value - The value to display in relation to the field.
+     * @throws NoSuchElementException Occurs if an invalid field name
+     * is provided for argument <code>fieldName</code>.
+     */
+    public final void setStatusField(String fieldName, String value) throws NoSuchElementException {
+        // Get the index for the indicated field.
+        Integer index = fieldMap.get(fieldName);
+        
+        // If it is null, the field does not exist.
+        if(index == null) { throw new NoSuchElementException("Field \"" + fieldName + "\" does not exist."); }
+        
+        // Otherwise, set the field.
+        else { statusPanel.setFieldValue(index, value); }
+    }
+    
+    /**
+     * Sets whether the calorimeter panel will highlight any crystals
+     * that the mouse cursor passes over.
+     * @param state - <code>true</code> indicates that crystals will
+     * be highlighted and <code>false</code> that they will not.
+     */
+    public void setUseCrystalHighlighting(boolean state) {
+        ecalPanel.setSelectionHighlighting(state);
+    }
+    
+    /**
+     * Sets whether zero-energy crystals should be rendered as grey or
+     * whether they should use the minimum-energy color from the color
+     * scale.
+     * @param state - <code>false</code> indicates that zero-energy 
+     * crystals will be rendered in grey and <code>true</code> that
+     * they will be rendered as per the color scale.
+     */
+    public void setUseZeroEnergyColorMapping(boolean state) {
+        if(state) { ecalPanel.setDefaultCrystalColor(null); }
+        else { ecalPanel.setDefaultCrystalColor(DEFAULT_CRYSTAL_COLOR); }
+    }
+    
+    /**
+     * Sets the calorimeter panel to be displayed using a linear scale.
+     */
+    public void setUseLinearScale() { ecalPanel.setScalingLinear(); }
+    
+    /**
+     * Sets the calorimeter panel to be displayed using a logarithmic
+     * scale.
+     */
+    public void setUseLogarithmicScale() { ecalPanel.setScalingLogarithmic(); }
     
     /**
      * Converts the calorimeter panel's coordinate pair to the LCSim
@@ -220,58 +525,63 @@
     public static final int toPanelY(int ecalY) { return 5 - ecalY; }
     
     /**
-     * Sets whether to mirror the x-axis on the calorimeter display.
-     * @param state - <code>true</code> indicates that the axis should
-     * be mirrored and <code>false</code> that it should not.
-     */
-    public void setMirrorX(boolean state) { ecalPanel.setMirrorX(state); }
-    
-    /**
-     * Sets whether to mirror the y-axis on the calorimeter display.
-     * @param state - <code>true</code> indicates that the axis should
-     * be mirrored and <code>false</code> that it should not.
-     */
-    public void setMirrorY(boolean state) { ecalPanel.setMirrorY(state); }
-    
-    /**
-     * Removes the specified crystal listener so that it no longer
-     * receives crystal events from this component. This method performs
-     * no function, nor does it throw an exception, if the listener
-     * specified by the argument was not previously added to this
-     * component. If listener <code>cl</code> is <code>null</code>, no
-     * exception is thrown and no action is performed. 
-     * @param cl - The listener to remove.
-     */
-    public void removeCrystalListener(CrystalListener cl) {
-        if(cl != null) { listenerList.remove(cl); }
-    }
-    
-    public void setSize(int width, int height) {
-        super.setSize(width, height);
-        resize();
-    }
-    
-    public void setSize(Dimension d) {
-        setSize(d.width, d.height);
-    }
-    
-    /**
-     * Sets the value of the indicated status field on the calorimeter
-     * display.
-     * @param fieldName - The name of the field to set.
-     * @param value - The value to display in relation to the field.
-     * @throws NoSuchElementException Occurs if an invalid field name
-     * is provided for argument <code>fieldName</code>.
-     */
-    public final void setStatusField(String fieldName, String value) throws NoSuchElementException {
-        // Get the index for the indicated field.
-        Integer index = fieldMap.get(fieldName);
-        
-        // If it is null, the field does not exist.
-        if(index == null) { throw new NoSuchElementException("Field \"" + fieldName + "\" does not exist."); }
-        
-        // Otherwise, set the field.
-        else { statusPanel.setFieldValue(index, value); }
+     * Indicates whether crystal will be highlighted when the cursor
+     * passes over them or not.
+     * @return Returns <code>true</code> if crystals will be highlighted
+     * and <code>false</code> if they will not.
+     */
+    public boolean usesCrystalHighlighting() {
+        return ecalPanel.isSelectionEnabled();
+    }
+    
+    /**
+     * Indicates whether zero-energy crystals are colored using the
+     * standard color scale or rendered in grey.
+     * @return Returns <code>false</code> if zero-energy crystal will
+     * be rendered in grey and <code>true</code> if they will use the
+     * regular color scale.
+     */
+    public boolean usesZeroEnergyColorMapping() {
+        return (ecalPanel.getDefaultCrystalColor() == null);
+    }
+    
+    /**
+     * Indicates whether the panel is scaled linearly.
+     * @return Returns <code>true</code> if the scaling is linear and
+     * <code>false</code> if the scaling is logarithmic.
+     */
+    public boolean usesLinearScale() {
+        return ecalPanel.isScalingLinear();
+    }
+    
+    /**
+     * Indicates whether the panel is scaled logarithmically.
+     * @return Returns <code>true</code> if the scaling is logarithmic
+     * and <code>false</code> if the scaling is linear.
+     */
+    public boolean usesLogarithmicScale() {
+        return ecalPanel.isScalingLogarithmic();
+    }
+    
+    /**
+     * Adds a new field to the status panel.
+     * @param fieldName - The name to display for the field and that
+     * links to the field when calling <code>setStatusField</code>.
+     */
+    protected final void addStatusField(String fieldName) {
+        fieldMap.put(fieldName, statusPanel.getFieldCount());
+        statusPanel.addField(fieldName);
+    }
+    
+    /**
+     * Inserts the field at the indicated location on the status panel.
+     * @param index - The index at which to insert the field.
+     * @param fieldName - The name to display for the field and that
+     * links to the field when calling <code>setStatusField</code>.
+     */
+    protected final void insertStatusField(int index, String fieldName) {
+        statusPanel.insertField(index, fieldName);
+        fieldMap = statusPanel.getFieldNameIndexMap();
     }
     
     /**
@@ -299,7 +609,7 @@
     
     /**
      * Handles proper resizing of the window and its components.
-     **/
+     */
     private void resize() {
         // Define the size constants.
         int statusHeight = 125;
@@ -309,8 +619,69 @@
         ecalPanel.setSize(getContentPane().getWidth(), getContentPane().getHeight() - statusHeight);
         
         // Size and position the status panel.
-        statusPanel.setLocation(0, ecalPanel.getHeight());
-        statusPanel.setSize(getContentPane().getWidth(), statusHeight);
+        statusScroller.setLocation(0, ecalPanel.getHeight());
+        statusScroller.setSize(getContentPane().getWidth(), statusHeight);
+        statusPanel.setSize(statusScroller.getViewport().getSize());
+    }
+    
+    /**
+     * Saves a screenshot to the application root directory.
+     */
+    private void saveScreenshot() {
+        // Make a new buffered image on which to draw the content pane.
+        BufferedImage screenshot = new BufferedImage(getContentPane().getWidth(),
+                getContentPane().getHeight(), BufferedImage.TYPE_INT_ARGB);
+        
+        // Paint the content pane to image.
+        getContentPane().paint(screenshot.getGraphics());
+        
+        // Get the lowest available file name.
+        int fileNum = 0;
+        File imageFile = new File("screenshot_" + fileNum + ".png");
+        while(imageFile.exists()) {
+            fileNum++;
+            imageFile = new File("screenshot_" + fileNum + ".png");
+        }
+        
+        // Save the image to a PNG file.
+        try { ImageIO.write(screenshot, "PNG", imageFile); }
+        catch(IOException ioe) {
+            System.err.println("Error saving file \"screenshot.png\".");
+        }
+        System.out.println("Screenshot saved to: " + imageFile.getAbsolutePath());
+    }
+    
+    /**
+     * Handles events generated by the scaling options radio buttons
+     * in the scaling menu.
+     * @param activatingIndex - The index of the radio button that
+     * triggered the event.
+     */
+    private void scaleChange(int activatingIndex) {
+        // If neither radio box is selected, then whichever caused the
+        // activation event was unselected. It should be selected again
+        // and the event ignored.
+        if(!menuScaling[MENU_ITEM_LIN_SCALE].isSelected() && !menuScaling[MENU_ITEM_LOG_SCALE].isSelected()) {
+            menuScaling[activatingIndex].setSelected(true);
+        }
+        
+        // Otherwise, whichever did not activate the event should be
+        // unselected and the scaling changed accordingly.
+        else {
+            // If linear scaling activated the event...
+            if(activatingIndex == MENU_ITEM_LIN_SCALE) {
+                menuScaling[MENU_ITEM_LIN_SCALE].setSelected(true);
+                menuScaling[MENU_ITEM_LOG_SCALE].setSelected(false);
+                setUseLinearScale();
+            }
+            
+            // If logarithmic scaling activated the event...
+            else {
+                menuScaling[MENU_ITEM_LIN_SCALE].setSelected(false);
+                menuScaling[MENU_ITEM_LOG_SCALE].setSelected(true);
+                setUseLogarithmicScale();
+            }
+        }
     }
     
     /**
@@ -318,7 +689,8 @@
      * and crystal field information when the cursor leaves the window.
      * It also triggers crystal click events.
      */
-    private class EcalMouseListener implements MouseListener {
+    private class EcalMouseListener extends MouseAdapter {
+        @Override
         public void mouseClicked(MouseEvent e) {
             // If there is a selected crystal, trigger a crystal click event.
             if(ecalPanel.getSelectedCrystal() != null) {
@@ -333,16 +705,11 @@
             }
         }
         
-        public void mouseEntered(MouseEvent e) { }
-        
+        @Override
         public void mouseExited(MouseEvent e) {
             ecalPanel.clearSelectedCrystal();
             statusPanel.clearFields();
         }
-        
-        public void mousePressed(MouseEvent e) { }
-        
-        public void mouseReleased(MouseEvent e) { }
     }
     
     /**
@@ -351,9 +718,8 @@
      * mouse moves over the window. Additionally triggers crystal
      * activation and deactivation events.
      */
-    private class EcalMouseMotionListener implements MouseMotionListener {        
-        public void mouseDragged(MouseEvent arg0) { }
-        
+    private class EcalMouseMotionListener extends MouseMotionAdapter {
+        @Override
         public void mouseMoved(MouseEvent e) {
             // Get the panel coordinates.
             int x = e.getX();
@@ -428,16 +794,39 @@
     }
     
     /**
-     * The <code>ResizeListener</code> class ensures that the components remain
-     * at the correct size and location when the window is resized.
-     **/
-    private class ResizeListener implements ComponentListener {
-        public void componentResized(ComponentEvent e) { resize(); }
-        
-        public void componentHidden(ComponentEvent e) { }
-        
-        public void componentMoved(ComponentEvent e) { }
-        
-        public void componentShown(ComponentEvent e) { }
-    }
-}
+     * Updates the settings panel whenever a tracked property in the
+     * calorimeter panel is updated.
+     * 
+     * @author Kyle McCarty
+     */
+    private class PropertyUpdater implements SettingsListener {
+        @Override
+        public void settingChanged(SettingsEvent e) {
+            // If the highlighting behavior has changed...
+            if(e.getID() == SettingsEvent.PROPERTY_HOVER_HIGHLIGHT) {
+                menuHighlight.setSelected(ecalPanel.isSelectionEnabled());
+            }
+            
+            // If the scaling type has changed...
+            else if(e.getID() == SettingsEvent.PROPERTY_SCALE_TYPE) {
+                menuScaling[MENU_ITEM_LIN_SCALE].setSelected(ecalPanel.isScalingLinear());
+                menuScaling[MENU_ITEM_LOG_SCALE].setSelected(ecalPanel.isScalingLogarithmic());
+            }
+            
+            // If the x-axis orientation has changed...
+            else if(e.getID() == SettingsEvent.PROPERTY_X_ORIENTATION) {
+                menuMirrorX.setSelected(ecalPanel.isMirroredX());
+            }
+            
+            // If the y-axis orientation has changed...
+            else if(e.getID() == SettingsEvent.PROPERTY_Y_ORIENTATION) {
+                menuMirrorY.setSelected(ecalPanel.isMirroredY());
+            }
+            
+            // If the zero-energy color mapping has changed...
+            else if(e.getID() == SettingsEvent.PROPERTY_ZERO_ENERGY_COLOR) {
+                menuBackground.setSelected(ecalPanel.getDefaultCrystalColor() == null);
+            }
+        }
+    }
+}

Added: java/trunk/ecal-event-display/src/main/java/org/hps/monitoring/ecal/eventdisplay/util/SettingsEvent.java
 =============================================================================
--- java/trunk/ecal-event-display/src/main/java/org/hps/monitoring/ecal/eventdisplay/util/SettingsEvent.java	(added)
+++ java/trunk/ecal-event-display/src/main/java/org/hps/monitoring/ecal/eventdisplay/util/SettingsEvent.java	Tue Nov  4 10:19:31 2014
@@ -0,0 +1,74 @@
+package org.hps.monitoring.ecal.eventdisplay.util;
+
+import java.awt.AWTEvent;
+
+import org.hps.monitoring.ecal.eventdisplay.ui.CalorimeterPanel;
+
+/**
+ * Class <code>SettingsEvent</code> represents a change that has occurred
+ * to some setting in a <code>CalorimeterPanel</code> object. It stores
+ * both the triggering object and an <code>int</code> primitive which
+ * corresponds to the particular setting that triggered the event.
+ * 
+ * @author Kyle McCarty
+ * @see CalorimeterPanel
+ */
+public class SettingsEvent extends AWTEvent {
+    // Local variables.
+    private static final long serialVersionUID = 1L;
+    
+    // Event IDs.
+    /**
+     * Indicates that the panel has changed its scaling to either
+     * linear or logarithmic.
+     */
+    public static final int PROPERTY_SCALE_TYPE = AWTEvent.RESERVED_ID_MAX + 1;
+    
+    /**
+     * Indicates that the panel's x-axis orientation has changed.
+     */
+    public static final int PROPERTY_X_ORIENTATION = AWTEvent.RESERVED_ID_MAX + 2;
+    
+    /**
+     * Indicates that the panel's y-axis orientation has changed.
+     */
+    public static final int PROPERTY_Y_ORIENTATION = AWTEvent.RESERVED_ID_MAX + 3;
+    
+    /**
+     * Indicates that the panel's behavior for highlighting crystals
+     * that are under the cursor has changed.
+     */
+    public static final int PROPERTY_HOVER_HIGHLIGHT = AWTEvent.RESERVED_ID_MAX + 4;
+    
+    /**
+     * Indicates that  the panel's behavior for coloring zero-energy
+     * crystals has changed.
+     */
+    public static final int PROPERTY_ZERO_ENERGY_COLOR = AWTEvent.RESERVED_ID_MAX + 5;
+    
+    /**
+     * Indicates that a change has occurred to the panel's scale minimum
+     * or maximum value.
+     */
+    public static final int PROPERTY_SCALE_RANGE = AWTEvent.RESERVED_ID_MAX + 6;
+    
+    /**
+     * Indicates that the panel's energy-to-color map has changed.
+     */
+    public static final int PROPERTY_SCALE_COLOR_MAP = AWTEvent.RESERVED_ID_MAX + 7;
+    
+    /**
+     * Indicates that the panel's scale has changed visibility.
+     */
+    public static final int PROPERTY_SCALE_VISIBLE = AWTEvent.RESERVED_ID_MAX + 8;
+    
+    /**
+     * Instantiates a new <code>SettingsEvent</code> object.
+     * @param source - The calorimeter panel that triggered the event.
+     * @param propertyID - The ID corresponding to the property that
+     * has changed value.
+     */
+    public SettingsEvent(CalorimeterPanel source, int propertyID) {
+        super(source, propertyID);
+    }
+}

Added: java/trunk/ecal-event-display/src/main/java/org/hps/monitoring/ecal/eventdisplay/util/SettingsListener.java
 =============================================================================
--- java/trunk/ecal-event-display/src/main/java/org/hps/monitoring/ecal/eventdisplay/util/SettingsListener.java	(added)
+++ java/trunk/ecal-event-display/src/main/java/org/hps/monitoring/ecal/eventdisplay/util/SettingsListener.java	Tue Nov  4 10:19:31 2014
@@ -0,0 +1,17 @@
+package org.hps.monitoring.ecal.eventdisplay.util;
+
+import java.util.EventListener;
+
+/**
+ * Interface <code>SettingsListener</code> tracks changes that occur
+ * in the settings of a <code>CalorimeterPanel</code> object.
+ * 
+ * @author Kyle McCarty
+ */
+public interface SettingsListener extends EventListener {
+    /**
+     * Indicates that a setting has changed.
+     * @param e - An event representing the change.
+     */
+    public void settingChanged(SettingsEvent e);
+}

Top of Message | Previous Page | Permalink

Advanced Options


Options

Log In

Log In

Get Password

Get Password


Search Archives

Search Archives


Subscribe or Unsubscribe

Subscribe or Unsubscribe


Archives

November 2017
August 2017
July 2017
January 2017
December 2016
November 2016
October 2016
September 2016
August 2016
July 2016
June 2016
May 2016
April 2016
March 2016
February 2016
January 2016
December 2015
November 2015
October 2015
September 2015
August 2015
July 2015
June 2015
May 2015
April 2015
March 2015
February 2015
January 2015
December 2014
November 2014
October 2014
September 2014
August 2014
July 2014
June 2014
May 2014
April 2014
March 2014
February 2014
January 2014
December 2013
November 2013

ATOM RSS1 RSS2



LISTSERV.SLAC.STANFORD.EDU

Secured by F-Secure Anti-Virus CataList Email List Search Powered by the LISTSERV Email List Manager

Privacy Notice, Security Notice and Terms of Use