erigo.ctstream.CTstream.java Source code

Java tutorial

Introduction

Here is the source code for erigo.ctstream.CTstream.java

Source

/*
Copyright 2017-2018 Erigo Technologies LLC
    
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
    
http://www.apache.org/licenses/LICENSE-2.0
    
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package erigo.ctstream;

import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.geom.Area;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import javax.imageio.ImageIO;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URISyntaxException;
import javax.swing.*;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.List;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;

import cycronix.ctlib.CTreader;

/**
 * 
 * Save various streams of data to CloudTurbine format (screen capture images, webcam
 * images, audio, text, etc.).
 *
 * Each stream is managed by its own class which extends DataStream.  Each DataStream
 * has a queue onto which the stream puts data to be sent to CloudTurbine.  WriteTask
 * takes data from each queue and sends it to CloudTurbine.
 * 
 * Classes involved in this application:
 * -------------------------------------
 * 01. CTstream: main class; manages the GUI, settings and program flow
 * 02. CTsettings: dialog to allow the user to edit settings
 * 03. DefineCaptureRegion: NO LONGER USED; we previously used this class to allow the
 *       user to select the region of the screen they wish to capture; this is now
 *       done dynamically by the user moving and resizing the main application frame
 * 04. DataStream: abstract class which is extended by each class implementing a data source
 * 05. ScreencapStream: extends DataStream; performs periodic screen captures
 * 06. WebcamStream: extends DataStream; periodically saves images from a web camera
 * 07. AudioStream: extends DataStream; captures audio WAV data
 * 08. TextStream: extends DataStream; send text from the TextArea UI control to CloudTurbine
 * 09. DataStreamSpec and its subclasses (ImageStreamSpec and ScreencapStreamSpec): data utility classes used
 *       to pass settings and object handles to a DataStream when it is being launched.
 * 10. TimeValue: a data sample to be sent to CloudTurbine, made up of byte array and a timestamp;
 *       TimeValue objects are generated by DataStream classes and stored in queues; WriteTask
 *       takes samples off the queues and sends them to CloudTurbine
 * 11. ImageTimerTask: used by ScreencapStream and WebcamStream; schedules for another image to be saved
 * 12. ImageTask: a task launched by ImageTimerTask; executes in its own thread; generates an image and saves it
 *       on a DataStream's queue
 * 13. PreviewWindow: a preview window to display the data generated by a DataStream
 * 14. WriteTask: handles all CloudTurbine API calls
 * 15. Utility: utility methods
 *
 * GUI notes
 * ---------
 * The JFrame that pops up will either be a Shaped window containing a "cut out" section (the user
 * will be able to reach through this cut out area and manipulate windows behind the JFrame) or
 * else contain a translucent panel (i.e., opacity less than 1.0).  The "cut out" section or the
 * translucent section define the image capture region.  Note that the Java 1.8 Javadoc for
 * java.awt.Frame.setOpacity() specifies:
 * 
 *  *  The following conditions must be met in order to set the opacity value less than 1.0f:
 *  *
 *  *      The TRANSLUCENT translucency must be supported by the underlying system
 *  *      The window must be undecorated (see setUndecorated(boolean) and Dialog.setUndecorated(boolean))
 *  *      The window must not be in full-screen mode (see GraphicsDevice.setFullScreenWindow(Window)) 
 *  *
 *  *  If the requested opacity value is less than 1.0f, and any of the above conditions are not met, the window opacity will not change, and the IllegalComponentStateException will be thrown. 
 * 
 * We see this behavior immediately upon CTstream startup under Mac OS if the JFrame is "decorated"
 * (an IllegalComponentStateException exception is thrown right away).  Surprisingly, for some unknown
 * reason, the transparency works with a *decorated* JFrame under Windows OS - however, we don't want
 * to count on that.  To be Java compliant, we won't use a decorated window.  This creates the challenge
 * of how do we support moving and resizing the window (without decoration on the window, there is no
 * inherent way to move and resize the window)?  We do this by creating our own simple window manager:
 * CTstream implements MouseMotionListener by defining mouseDragged() and mouseMoved().
 *
 * For information and examples on translucent and shaped windows, see the following:
 * https://docs.oracle.com/javase/tutorial/uiswing/misc/trans_shaped_windows.html
 * 
 * "Continue" mode
 * ---------------
 * To produce a video with several different "takes", CTstream supports "Continue" mode.  After
 * CTstream has started, when the user is finished with the scene they can "Stop", make any
 * necessary adjustments and then "Continue".  CTstream sets the next timestamp equal to
 * (the last timestamp + 1) after starting up in continue mode.
 * 
 * All CloudTurbine timestamps are created in getNextTime().  If the user is in standard
 * (not continue) mode, getNextTime() returns System.currentTimeMillis(); in continue mode,
 * getNextTime() will calculate the appropriate timestamp to use to maintain seamless
 * video/audio playback.
 *
 * Known issues
 * ------------
 * 01. Screen capture does not work under Ubuntu (possibly under other flavors of Linux as well); the captured image is
 *     black.
 * 02. Under Mac OS, the user can't "reach through" the capture frame to interact with windows behind it.
 * 
 * @author John P. Wilson, Matt J. Miller
 * @version 03/07/2018
 *
 */
public class CTstream implements ActionListener, MouseMotionListener {

    // Constants used by the UI
    private final static double DEFAULT_FPS = 5.0; // default images/sec
    private final static Double[] FPS_VALUES = { 0.1, 0.2, 0.5, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0 };
    private final static double AUTO_FLUSH_DEFAULT = 1.0; // default auto-flush in seconds

    private static int dataStreamID = -1; // IDs applied to DataStreams to identify them.

    //
    // Settings
    //
    // Setting made in the main control panel
    private boolean bScreencap = false; // stream screencap image data?
    private boolean bWebcam = false; // stream webcam image data?
    private boolean bAudio = false; // stream audio data?
    private boolean bText = false; // stream text data?
    private double framesPerSec; // how many frames to capture per second
    private float imageQuality = 0.70f; // Image quality; 0.00 - 1.00; higher numbers correlate to better quality/less compression
    private boolean bChangeDetect = false; // detect and record only images that change (more CPU, less storage)
    private boolean bFullScreen = false; // capture the full screen?
    private boolean bIncludeMouseCursor = true; // include the mouse cursor in the screencap image?
    private boolean bPreview = false; // Does the user want to display the preview windows?
    // Settings made in the Settings dialog, CTsettings
    private CTsettings ctSettings = null; // GUI to view/edit settings
    public String outputFolder = "CTdata"; // location of output files
    public String sourceName = "CTstream"; // output source name
    private String screencapStreamName = "screencap.jpg"; // screencap channel name; must end in ".jpg" or ".jpeg"
    private String webcamStreamName = "webcam.jpg"; // webcam channel name; must end in ".jpg" or ".jpeg"
    private String audioStreamName = "audio.wav"; // audio channel name; must end in ".wav"
    private String textStreamName = "text.txt"; // text channel name; must end in ".txt"
    public boolean bEncrypt = false; // Use CT encryption?
    public String encryptionPassword = ""; // Password when encryption is on

    public enum CTWriteMode { // Possible modes for writing out CT data which CTstream supports
        LOCAL, FTP, HTTP, HTTPS
    }

    public CTWriteMode writeMode = CTWriteMode.LOCAL; // The selected mode for writing out CT data
    public String serverHost = ""; // Server (FTP or HTTP) hostname
    public String serverUser = ""; // Server (FTP or HTTP) username
    public String serverPassword = ""; // Server (FTP or HTTP) password
    public long flushMillis; // flush interval (msec)
    public long numBlocksPerSegment = 0; // number of blocks per segment; defaults to 0 (no segments)
    public boolean bDebugMode = false; // run CT in debug mode?
    public boolean bPrintDataStatusMsg = false; // print single-character status updates as data is added to the queue and then sent to CT?
    public boolean bStayOnTop = false; // keep the CTstream UI on top of all other windows on the desktop

    // Output ZIP files?  Considered an expert option; turning off ZIP can only be done via command line flag.
    public boolean bZipMode = true;

    // To control CT shutdown
    private boolean bCallExitFromShutdownHook = true;

    // DataStream classes
    public Vector<DataStream> dataStreams = new Vector<DataStream>(); // to store active DataStreams
    public Object dataStreamsLock = new Object(); // To provide synchronized access to Vector dataStreams
    public ScreencapStream screencapStream = null; // take periodic screen captures
    public WebcamStream webcamStream = null; // save images from a web camera
    public AudioStream audioStream = null; // save audio data
    public TextStream textStream = null; // save text
    public DocumentChangeListener docChangeListener = null; // Responds to user edits by saving entire document in TextStream's queue

    // Class which manages all CT API calls; takes data from each DataStream's queue and writes it to CT
    private WriteTask writeTask = null;

    // Variables to support "continue" mode, where DataStreams resume, picking up in time just where they left off;
    // kind of like a seamless "pause"
    private boolean bContinueMode = false; // Are we in continue mode?
    private long firstCTtime = 0; // First timestamp used in a "Continue" segment
    private long lastCTtime = 0; // In "Continue" mode, the last timestamp we used
    private long continueWallclockInitTime = 0; // Wallclock time when continue starts up.

    // Main panel GUI objects
    public JFrame guiFrame = null; // JFrame which contains translucent panel which defines the capture region
    private JPanel guiPanel = null; // top-level panel that will contain all UI components
    private int guiFrameOrigHeight = -1; // used when clicking the Checkbox to go between capturing the region defined by capturePanel and full screen
    private JPanel controlsPanel = null; // child of guiPanel; contains the controls at the top of the GUI
    public JPanel capturePanel = null; // translucent panel which defines the region to capture
    private JCheckBox screencapCheck = null; // checkbox to turn on/off image capture from screen
    private JCheckBox webcamCheck = null; // checkbox to turn on/off image capture from camera
    private JCheckBox audioCheck = null; // checkbox to turn on/off audio capture
    private JCheckBox textCheck = null; // checkbox to turn on/off text capture
    private JComboBox<Double> fpsCB = null; // frames/sec checkbox
    private JSlider imgQualSlider = null; // image quality slider
    private JCheckBox includeMouseCursorCheck = null; // Include the mouse cursor in the screen capture image?
    private JCheckBox changeDetectCheck = null; // checkbox to turn on/off "change detect"
    private JCheckBox fullScreenCheck = null; // checkbox to turn on/off doing full screen capture
    private JCheckBox previewCheck = null; // checkbox to turn on/off the preview window
    public JTextArea textArea = null; // text area associated with TextStream
    private JScrollPane textScrollPane = null; // scrollpane for the text area
    private JButton startStopButton = null; // One button to Start and then Stop streaming data
    private JButton continueButton = null; // Clicking this is just like clicking "Start" except we pick up in time
    // just where we left off; kind of like a seamless "pause"

    // Since translucent panels can only be contained within undecorated Frames (see comments in header above)
    // and since undecorated Frames don't support moving/resizing, we implement our own basic "window manager"
    // by catching mouse move and drag events (see mouseDragged() and mouseMoved() methods below).  The following
    // members are used in this implementation.
    private final static int NO_COMMAND = 0;
    private final static int MOVE_FRAME = 1;
    private final static int RESIZE_FRAME_NW = 2;
    private final static int RESIZE_FRAME_N = 3;
    private final static int RESIZE_FRAME_NE = 4;
    private final static int RESIZE_FRAME_E = 5;
    private final static int RESIZE_FRAME_SE = 6;
    private final static int RESIZE_FRAME_S = 7;
    private final static int RESIZE_FRAME_SW = 8;
    private final static int RESIZE_FRAME_W = 9;
    private int mouseCommandMode = NO_COMMAND;
    private Point mouseStartingPoint = null;
    private Rectangle frameStartingBounds = null;

    // Port for viewing CT data from WebScan
    public int webScanPort = 8000;
    // Other options when launching CTweb
    public String otherCTwebOptions = "";

    /**
     *
     * Main function; creates an instance of CTstream
     * 
     * @param argsI  Command line arguments
     */
    public static void main(String[] argsI) {
        new CTstream(argsI);
    }

    /**
     * 
     * CTstream constructor
     * 
     * @param argsI  Command line arguments
     */
    public CTstream(String[] argsI) {

        // Create a String version of FPS_VALUES
        String FPS_VALUES_STR = new String(FPS_VALUES[0].toString());
        for (int i = 1; i < FPS_VALUES.length; ++i) {
            FPS_VALUES_STR = FPS_VALUES_STR + "," + FPS_VALUES[i].toString();
        }

        // Create a String version of CTsettings.flushIntervalLongs
        String FLUSH_VALUES_STR = String.format("%.1f", CTsettings.flushIntervalLongs[0] / 1000.0);
        for (int i = 1; i < CTsettings.flushIntervalLongs.length; ++i) {
            // Except for the first flush value (handled above, first item in the string) the rest of these
            // are whole numbers, so we will print them out with no decimal places
            FLUSH_VALUES_STR = FLUSH_VALUES_STR + ","
                    + String.format("%.0f", CTsettings.flushIntervalLongs[i] / 1000.0);
        }

        //
        // Parse command line arguments
        //
        // 1. Setup command line options
        //
        Options options = new Options();
        // Boolean options (only the flag, no argument)
        options.addOption("h", "help", false, "Print this message.");
        options.addOption("nm", "no_mouse_cursor", false,
                "Don't include mouse cursor in output screen capture images.");
        options.addOption("nz", "no_zip", false, "Turn off ZIP output.");
        options.addOption("x", "debug", false, "Turn on debug output.");
        options.addOption("cd", "change_detect", false, "Save only changed images.");
        options.addOption("fs", "full_screen", false, "Capture the full screen.");
        options.addOption("p", "preview", false, "Display live preview image");
        options.addOption("T", "UI_on_top", false, "Keep CTstream on top of all other windows.");
        options.addOption("sc", "screencap", false, "Capture screen images");
        options.addOption("w", "webcam", false, "Capture webcam images");
        options.addOption("a", "audio", false, "Record audio.");
        options.addOption("t", "text", false, "Capture text.");

        // Command line options that include a flag
        // For example, the following will be for "-outputfolder <folder>   (Location of output files...)"
        Option option = Option.builder("o").longOpt("outputfolder").argName("folder").hasArg()
                .desc("Location of output files (source is created under this folder); default = \"" + outputFolder
                        + "\".")
                .build();
        options.addOption(option);
        option = Option.builder("fps").argName("framespersec").hasArg().desc(
                "Image rate (images/sec); default = " + DEFAULT_FPS + "; accepted values = " + FPS_VALUES_STR + ".")
                .build();
        options.addOption(option);
        option = Option.builder("f").argName("autoFlush").hasArg()
                .desc("Flush interval (sec); amount of data per block; default = "
                        + Double.toString(AUTO_FLUSH_DEFAULT) + "; accepted values = " + FLUSH_VALUES_STR + ".")
                .build();
        options.addOption(option);
        option = Option.builder("s").argName("source name").hasArg()
                .desc("Name of output source; default = \"" + sourceName + "\".").build();
        options.addOption(option);
        option = Option.builder("sc_chan").argName("screencap_chan_name").hasArg()
                .desc("Screen capture image channel name (must end in \".jpg\" or \".jpeg\"); default = \""
                        + screencapStreamName + "\".")
                .build();
        options.addOption(option);
        option = Option.builder("webcam_chan").argName("webcam_chan_name").hasArg()
                .desc("Web camera image channel name (must end in \".jpg\" or \".jpeg\"); default = \""
                        + webcamStreamName + "\".")
                .build();
        options.addOption(option);
        option = Option.builder("audio_chan").argName("audio_chan_name").hasArg()
                .desc("Audio channel name (must end in \".wav\"); default = \"" + audioStreamName + "\".").build();
        options.addOption(option);
        option = Option.builder("text_chan").argName("text_chan_name").hasArg()
                .desc("Text channel name (must end in \".txt\"); default = \"" + textStreamName + "\".").build();
        options.addOption(option);
        option = Option.builder("q").argName("imagequality").hasArg()
                .desc("Image quality, 0.00 - 1.00 (higher numbers are better quality); default = "
                        + Float.toString(imageQuality) + ".")
                .build();
        options.addOption(option);

        //
        // 2. Parse command line options
        //
        CommandLineParser parser = new DefaultParser();
        CommandLine line = null;
        try {
            line = parser.parse(options, argsI);
        } catch (ParseException exp) {
            // oops, something went wrong
            System.err.println("Command line argument parsing failed: " + exp.getMessage());
            return;
        }

        //
        // 3. Retrieve the command line values
        //
        if (line.hasOption("help")) {
            // Display help message and quit
            HelpFormatter formatter = new HelpFormatter();
            formatter.setWidth(160);
            formatter.printHelp("CTstream", options);
            System.exit(0);
        }
        bPreview = line.hasOption("preview");
        bScreencap = line.hasOption("screencap");
        bWebcam = line.hasOption("webcam");
        bAudio = line.hasOption("audio");
        bText = line.hasOption("text");
        // Source name
        sourceName = line.getOptionValue("s", sourceName);
        // Where to write the files to
        outputFolder = line.getOptionValue("outputfolder", outputFolder);
        // Channel names; check filename extensions
        screencapStreamName = line.getOptionValue("sc_chan", screencapStreamName);
        webcamStreamName = line.getOptionValue("webcam_chan", webcamStreamName);
        audioStreamName = line.getOptionValue("audio_chan", audioStreamName);
        textStreamName = line.getOptionValue("text_chan", textStreamName);
        try {
            checkFilenames();
        } catch (Exception e) {
            System.err.println(e.getMessage());
            return;
        }
        // Auto-flush time
        try {
            double autoFlush = Double.parseDouble(line.getOptionValue("f", "" + AUTO_FLUSH_DEFAULT));
            flushMillis = (long) (autoFlush * 1000.);
            boolean bGotMatch = false;
            for (int i = 0; i < CTsettings.flushIntervalLongs.length; ++i) {
                if (flushMillis == CTsettings.flushIntervalLongs[i]) {
                    bGotMatch = true;
                    break;
                }
            }
            if (!bGotMatch) {
                throw new NumberFormatException("bad input value");
            }
        } catch (NumberFormatException nfe) {
            System.err.println("\nAuto flush time must be one of the following values: " + FLUSH_VALUES_STR);
            return;
        }
        // ZIP output files? (the only way to turn off ZIP is using this command line flag)
        bZipMode = !line.hasOption("no_zip");
        // Include cursor in output screen capture images?
        bIncludeMouseCursor = !line.hasOption("no_mouse_cursor");
        // How many frames (screen or webcam images) to capture per second
        try {
            framesPerSec = Double.parseDouble(line.getOptionValue("fps", "" + DEFAULT_FPS));
            if (framesPerSec <= 0.0) {
                throw new NumberFormatException("value must be greater than 0.0");
            }
            // Make sure framesPerSec is one of the accepted values
            Double userVal = framesPerSec;
            if (!Arrays.asList(FPS_VALUES).contains(userVal)) {
                throw new NumberFormatException(new String("framespersec value must be one of: " + FPS_VALUES_STR));
            }
        } catch (NumberFormatException nfe) {
            System.err.println(
                    "\nError parsing \"fps\"; it must be one of the accepted floating point values:\n" + nfe);
            return;
        }
        // Run CT in debug mode?
        bDebugMode = line.hasOption("debug");
        // changeDetect mode? MJM
        bChangeDetect = line.hasOption("change_detect");
        // Capture the full screen?
        bFullScreen = line.hasOption("full_screen");
        // Keep CTstream UI on top of all other windows?
        bStayOnTop = line.hasOption("UI_on_top");
        // Image quality
        String imageQualityStr = line.getOptionValue("q", "" + imageQuality);
        try {
            imageQuality = Float.parseFloat(imageQualityStr);
            if ((imageQuality < 0.0f) || (imageQuality > 1.0f)) {
                throw new NumberFormatException("");
            }
        } catch (NumberFormatException nfe) {
            System.err.println("\nimage quality must be a number in the range 0.0 <= x <= 1.0");
            return;
        }

        //
        // Specify a shutdown hook to catch Ctrl+c
        //
        final CTstream temporaryCTS = this;
        Runtime.getRuntime().addShutdownHook(new Thread() {
            @Override
            public void run() {
                if (bCallExitFromShutdownHook) {
                    temporaryCTS.exit(true);
                }
            }
        });

        // For now, we are going to assume that shaped windows (PERPIXEL_TRANSPARENT) are supported.
        // For a discussion of translucent and shaped windows see https://docs.oracle.com/javase/tutorial/uiswing/misc/trans_shaped_windows.html
        // This includes examples on how to check if TRANSLUCENT and PERPIXEL_TRANSPARENT are supported by the graphics.
        // For thread safety: Schedule a job for the event-dispatching thread to create and show the GUI
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                temporaryCTS.createAndShowGUI(true);
            }
        });

    }

    /**
     * Get the name of the screencap stream output CT channel.
     * @return name of the screencap stream output CT channel
     */
    public String getScreencapStreamName() {
        return screencapStreamName;
    }

    /**
     * Set the name of the screencap stream output CT channel.
     * @param nameI  The name of the screencap stream output CT channel.
     */
    public void setScreencapStreamName(String nameI) {
        screencapStreamName = nameI;
    }

    /**
     * Get the name of the webcam stream output CT channel.
     * @return name of the webcam stream output CT channel
     */
    public String getWebcamStreamName() {
        return webcamStreamName;
    }

    /**
     * Set the name of the webcam stream output CT channel.
     * @param nameI  The name of the webcam stream output CT channel.
     */
    public void setWebcamStreamName(String nameI) {
        webcamStreamName = nameI;
    }

    /**
     * Get the name of the audio stream output CT channel.
     * @return name of the audio stream output CT channel
     */
    public String getAudioStreamName() {
        return audioStreamName;
    }

    /**
     * Set the name of the audio stream output CT channel.
     * @param nameI  The name of the audio stream output CT channel.
     */
    public void setAudioStreamName(String nameI) {
        audioStreamName = nameI;
    }

    /**
     * Get the name of the text stream output CT channel.
     * @return name of the text stream output CT channel
     */
    public String getTextStreamName() {
        return textStreamName;
    }

    /**
     * Set the name of the text stream output CT channel.
     * @param nameI  The name of the text stream output CT channel.
     */
    public void setTextStreamName(String nameI) {
        textStreamName = nameI;
    }

    /**
     * Check channel names of the various DataStreams.
     * @throws Exception if there is any error in a channel name.
     */
    public void checkFilenames() throws Exception {
        try {
            checkFilename(screencapStreamName, new String[] { "jpg", "jpeg" });
        } catch (Exception e) {
            throw new Exception("Screencap channel name error: " + e.getMessage());
        }
        try {
            checkFilename(webcamStreamName, new String[] { "jpg", "jpeg" });
        } catch (Exception e) {
            throw new Exception("Webcam channel name error: " + e.getMessage());
        }
        try {
            checkFilename(audioStreamName, new String[] { "wav" });
        } catch (Exception e) {
            throw new Exception("Audio channel name error: " + e.getMessage());
        }

        try {
            checkFilename(textStreamName, new String[] { "txt" });
        } catch (Exception e) {
            throw new Exception("Text channel name error: " + e.getMessage());
        }
    }

    /**
     * Check the given filename: can't contain embedded spaces; must use one of the given extensions.
     * @param filename              The filename to check
     * @param acceptedExtensions    List of acceptable extensions
     * @throws Exception            Exception thrown if any error in the filename is detected
     */
    private static void checkFilename(String filename, String[] acceptedExtensions) throws Exception {
        if ((filename == null) || (filename.isEmpty())) {
            throw new Exception("filename is empty");
        }
        if (filename.contains(" ")) {
            throw new Exception("filename contains one or more embedded spaces");
        }
        if ((acceptedExtensions == null) || (acceptedExtensions.length == 0)) {
            // no extensions specified; this is OK - we're done
            return;
        }
        // Create a string containing all extensions (just in case we need it for returned error messages)
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < acceptedExtensions.length; ++i) {
            if (i > 0) {
                builder.append(",");
            }
            builder.append("\"" + acceptedExtensions[i] + "\"");
        }
        String extensionsStr = builder.toString();
        // Check the filename extension
        int dotIdx = filename.lastIndexOf('.');
        if ((dotIdx == -1) || (dotIdx == 0) || (dotIdx == (filename.length() - 1))) {
            throw new Exception("filename must end in one of the following extensions: " + extensionsStr);
        }
        String filenameExt = filename.substring(dotIdx + 1).toLowerCase();
        // Check this filename against the list of accepted extensions
        boolean bFoundExt = false;
        for (int i = 0; i < acceptedExtensions.length; ++i) {
            if (filenameExt.equals(acceptedExtensions[i])) {
                bFoundExt = true;
                break;
            }
        }
        if (!bFoundExt) {
            throw new Exception("filename must end in one of the following extensions: " + extensionsStr);
        }
    }

    /**
     * Static method to return a unique ID, used for identifying DataStreams.
     *
     * @return the next unique integer ID
     */
    public static int getNextDataStreamID() {
        dataStreamID = dataStreamID + 1;
        return dataStreamID;
    }

    /**
     * getNextTime
     * 
     * Calculate the next time to assign to a data point to be sent to CT.
     * The time to use depends on whether or not we are in continue mode.
     * 
     * @return The next time to assign to CT data.
     */
    public synchronized long getNextTime() {
        long nextTime = System.currentTimeMillis();
        if (bContinueMode) {
            if (firstCTtime == 0) {
                // Starting a new video segment ("continue" mode)
                if (writeMode != CTWriteMode.LOCAL) {
                    // Since we can't interrogate the remote source to determine
                    // the last timestamp, pickup at 1msec after the last timestamp
                    // we sent to CTwriter.
                    firstCTtime = lastCTtime + 1;
                } else {
                    // We are writing CT to local files.
                    // Pickup at 1msec after the last timestamp observed in the output source folders;
                    // this avoids a problem if user has manually deleted/changed CT disk folders
                    firstCTtime = 1 + (long) ((new CTreader(outputFolder).newTime(sourceName)) * 1000.);
                }
                // Note the current wall clock time
                continueWallclockInitTime = System.currentTimeMillis();
            }
            nextTime = firstCTtime + (System.currentTimeMillis() - continueWallclockInitTime);
            // Should we reject a backward going time?
            // o When writing to local files (ie, *not* FTP or HTTP mode) note that the user
            //   may have manually deleted/adjusted folders and so it may appear that
            //   the source time is going backward from the standpoint of the last
            //   timestamp we actually wrote out (lastCTtime); it is OK to write out
            //   a "backward going timestamp" in this case.
            // o When we are in FTP or HTTP mode, there's no way to know the latest time
            //   in the output source folders, thus it seems best to simply reject
            //   what looks to be a backward going time.
            // NO, let audioStream logic cover sane timestamps - MJM 4/4/17
            // if ( (writeMode != CTWriteMode.LOCAL) && (nextTime < lastCTtime) ) {
            //       System.err.println("\ngetNextTime: detected backward moving time; just return lastCTtime");
            //     nextTime = lastCTtime;
            // }
        }
        // Squirrel away this time
        lastCTtime = nextTime;
        return nextTime;
    }

    /**
     * Method to start WriteTask and all DataStreams
     */
    private void startCapture() {

        // Firewalls
        // By the time we get to this function, ctw should be null (ie, no currently active CT connection)
        if (audioStream != null) {
            System.err.println("ERROR in startCapture(): AudioStream object is not null; returning");
            return;
        }
        if (screencapStream != null) {
            System.err.println("ERROR in startCapture(): ScreencapStream object is not null; returning");
            return;
        }
        if (webcamStream != null) {
            System.err.println("ERROR in startCapture(): WebcamStream object is not null; returning");
            return;
        }
        if (writeTask != null) {
            System.err.println("ERROR in startCapture(): WriteTask object is not null; returning");
            return;
        }

        // Display current time
        String currTimeStr = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
        long currTime = System.currentTimeMillis();
        String datetimeStr = new String(currTimeStr + " (" + currTime + ")");
        if (bContinueMode) {
            System.err.println("\n" + datetimeStr + ": Continue streaming data from where we left off");
        } else {
            System.err.println("\n" + datetimeStr + ": Start streaming data");
        }

        try {
            writeTask = new WriteTask(this);
        } catch (IOException ioe) {
            System.err.println("Error trying to create CloudTurbine writer object:\n" + ioe);
            writeTask = null;
            stopCapture();
            return;
        }

        if (!startTextCapture()) {
            return;
        }

        if (!startScreencapCapture()) {
            return;
        }

        if (!startWebcamCapture()) {
            return;
        }

        if (!startAudioCapture()) {
            return;
        }

        // Now that all the DataStreams have been started, start WriteTask
        writeTask.start();

    }

    /**
     * Method to stop WriteTask and all DataStreams
     */
    public void stopCapture() {

        String currTimeStr = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
        long currTime = System.currentTimeMillis();
        String datetimeStr = new String(currTimeStr + " (" + currTime + ")");
        System.err.println("\n\n" + datetimeStr + ": Stop streams");

        // shut down WriteTask
        if (writeTask != null) {
            writeTask.stop();
            writeTask = null;
        }

        stopTextCapture();

        stopScreencapCapture();

        stopWebcamCapture();
        // Also close the web camera
        WebcamStream.closeWebCamera();

        stopAudioCapture();

    }

    /**
     * Add a new DataStream to Vector dataStreams.
     * @param dataStreamI  The DataStream to add to Vector dataStreams.
     */
    private void addDataStream(DataStream dataStreamI) {
        synchronized (dataStreamsLock) {
            dataStreams.add(dataStreamI);
            // System.err.println("\nadded DataStream; Vector size = " + dataStreams.size());
        }
    }

    /**
     * Remove a DataStream from Vector dataStreams.
     * @param dataStreamI  The DataStream to remove from Vector dataStreams.
     * @throws Exception   Throw exception if the given DataStream is not found in dataStreams.
     */
    private void removeDataStream(DataStream dataStreamI) throws Exception {
        boolean bRemoved = false;
        synchronized (dataStreamsLock) {
            bRemoved = dataStreams.remove(dataStreamI);
            // System.err.println("\nremoved DataStream; Vector size = " + dataStreams.size());
        }
        if (!bRemoved) {
            throw new Exception("The following DataStream could not be removed from the Vector of DataStreams: "
                    + dataStreamI.toString());
        }
    }

    /**
     * If user has requested it, start text capture.
     * @return false if an error occurred
     */
    private boolean startTextCapture() {
        if (bText && (textStream == null)) {
            DataStreamSpec spec = new DataStreamSpec();
            spec.cts = this;
            spec.channelName = textStreamName;
            spec.bPreview = bPreview;
            textStream = new TextStream(spec);
            try {
                textStream.start();
            } catch (Exception e) {
                System.err.println("Error starting text stream:\n" + e);
                textStream = null;
                stopCapture();
                return false;
            }
            addDataStream(textStream);
        }
        return true;
    }

    /**
     * Stop text capture.
     */
    private void stopTextCapture() {
        if (textStream != null) {
            textStream.stop();
            try {
                removeDataStream(textStream);
            } catch (Exception e) {
                System.err.println(e);
            }
            textStream = null;
        }
    }

    /**
     * If user has requested it, start screencap capture.
     * @return false if an error occurred
     */
    private boolean startScreencapCapture() {
        if (bScreencap && (screencapStream == null)) {
            ScreencapStreamSpec spec = new ScreencapStreamSpec();
            spec.cts = this;
            spec.channelName = screencapStreamName;
            spec.framesPerSec = ((Double) fpsCB.getSelectedItem()).doubleValue();
            spec.imageQuality = ((float) imgQualSlider.getValue()) / 1000.0f;
            spec.bPreview = bPreview;
            spec.bFullScreen = bFullScreen;
            spec.bIncludeMouseCursor = bIncludeMouseCursor;
            spec.bChangeDetect = bChangeDetect;
            screencapStream = new ScreencapStream(spec);
            try {
                screencapStream.start();
            } catch (Exception e) {
                System.err.println("Error starting screencap stream:\n" + e);
                screencapStream = null;
                stopCapture();
                return false;
            }
            addDataStream(screencapStream);
        }
        return true;
    }

    /**
     * Stop screencap capture.
     */
    private void stopScreencapCapture() {
        if (screencapStream != null) {
            screencapStream.stop();
            try {
                removeDataStream(screencapStream);
            } catch (Exception e) {
                System.err.println(e);
            }
            screencapStream = null;
        }
    }

    /**
     * If user has requested it, start webcam capture.
     * @return false if an error occurred
     */
    private boolean startWebcamCapture() {
        if (bWebcam && (webcamStream == null)) {
            ImageStreamSpec spec = new ImageStreamSpec();
            spec.cts = this;
            spec.channelName = webcamStreamName;
            spec.framesPerSec = ((Double) fpsCB.getSelectedItem()).doubleValue();
            spec.imageQuality = ((float) imgQualSlider.getValue()) / 1000.0f;
            spec.bPreview = bPreview;
            spec.bChangeDetect = bChangeDetect;
            webcamStream = new WebcamStream(spec);
            try {
                webcamStream.start();
            } catch (Exception e) {
                System.err.println("Error starting webcam stream:\n" + e);
                webcamStream = null;
                stopCapture();
                return false;
            }
            addDataStream(webcamStream);
        }
        return true;
    }

    /**
     * Stop webcam capture.
     */
    private void stopWebcamCapture() {
        if (webcamStream != null) {
            webcamStream.stop();
            try {
                removeDataStream(webcamStream);
            } catch (Exception e) {
                System.err.println(e);
            }
            webcamStream = null;
        }
    }

    /**
     * If user has requested it, start audio capture.
     * @return false if an error occurred
     */
    private boolean startAudioCapture() {
        if (bAudio && (audioStream == null)) {
            DataStreamSpec spec = new DataStreamSpec();
            spec.cts = this;
            spec.channelName = audioStreamName;
            spec.bPreview = bPreview;
            audioStream = new AudioStream(spec);
            try {
                audioStream.start();
            } catch (Exception e) {
                System.err.println("Error starting audio stream:\n" + e);
                audioStream = null;
                stopCapture();
                return false;
            }
            addDataStream(audioStream);
        }
        return true;
    }

    /**
     * Stop audio capture.
     */
    private void stopAudioCapture() {
        if (audioStream != null) {
            audioStream.stop();
            try {
                removeDataStream(audioStream);
            } catch (Exception e) {
                System.err.println(e);
            }
            audioStream = null;
        }
    }

    /*
     * Determine if appropriate settings have been made such that we can start streaming data.
     */
    public String canCTrun() {

        // Check sourceName
        if ((sourceName == null) || (sourceName.length() == 0)) {
            return "You must specify a source name.";
        }

        // Check filenames
        try {
            checkFilenames();
        } catch (Exception e) {
            return e.getMessage();
        }

        // Check that a password has been specified, if data encryption is turned on
        if (bEncrypt) {
            if ((encryptionPassword == null) || (encryptionPassword.length() == 0)) {
                return "You must specify the data encryption password";
            }
        }

        // Check write mode settings
        if (writeMode == CTWriteMode.LOCAL) {
            if ((outputFolder == null) || (outputFolder.length() == 0)) {
                return "You must specify an output directory.";
            }
        } else {
            if ((serverHost == null) || (serverHost.length() == 0)) {
                return "You must specify the server host";
            }
            if (serverHost.contains(" ")) {
                return "The server host name must not contain embedded spaces";
            }
            // serverUser is only required for FTP
            if ((writeMode == CTWriteMode.FTP) && ((serverUser == null) || (serverUser.length() == 0))) {
                return "You must specify the server username";
            }
            if (((writeMode == CTWriteMode.FTP) || (writeMode == CTWriteMode.HTTPS)) && serverUser.contains(" ")) {
                // username can be used with FTP or HTTPS;
                // if we are in one of those modes and the username contains a space, flag it as an error
                return "The server username must not contain embedded spaces";
            }
            // serverPassword is only required for FTP
            if ((writeMode == CTWriteMode.FTP) && ((serverPassword == null) || (serverPassword.length() == 0))) {
                return "You must specify the server password";
            }
        }

        if (flushMillis < CTsettings.flushIntervalLongs[0]) {
            return new String(
                    "Flush interval must be greater than or equal to " + CTsettings.flushIntervalLongs[0]);
        }

        return "";
    }

    /**
     * Pop up the GUI
     * 
     * This method should be run in the event-dispatching thread.
     * 
     * The GUI is created in one of two modes depending on whether Shaped
     * windows are supported on the platform:
     * 
     * 1. If Shaped windows are supported then guiPanel (the container to
     *    which all other components are added) is RED and capturePanel is
     *    inset a small amount to this panel so that the RED border is seen
     *    around the outer edge.  A componentResized() method is defined
     *    which creates the hollowed out region that was capturePanel.
     * 2. If Shaped windows are not supported then guiPanel is transparent
     *    and capturePanel is translucent.  In this case, the user can't
     *    "reach through" capturePanel to interact with GUIs on the other
     *    side.
     * 
     * @param  bShapedWindowSupportedI  Does the underlying GraphicsDevice support the
     *                            PERPIXEL_TRANSPARENT translucency that is
     *                            required for Shaped windows?
     */
    private void createAndShowGUI(boolean bShapedWindowSupportedI) {

        // No window decorations for translucent/transparent windows
        // (see note below)
        // JFrame.setDefaultLookAndFeelDecorated(true);

        //
        // Create GUI components
        //
        GridBagLayout framegbl = new GridBagLayout();
        guiFrame = new JFrame("CTstream");
        // To support a translucent window, the window must be undecorated
        // See notes in the class header up above about this; also see
        // http://alvinalexander.com/source-code/java/how-create-transparenttranslucent-java-jframe-mac-os-x
        guiFrame.setUndecorated(true);
        // Use MouseMotionListener to implement our own simple "window manager" for moving and resizing the window
        guiFrame.addMouseMotionListener(this);
        guiFrame.setBackground(new Color(0, 0, 0, 0));
        guiFrame.getContentPane().setBackground(new Color(0, 0, 0, 0));
        GridBagLayout gbl = new GridBagLayout();
        guiPanel = new JPanel(gbl);
        // if Shaped windows are supported, make guiPanel red;
        // otherwise make it transparent
        if (bShapedWindowSupportedI) {
            guiPanel.setBackground(Color.RED);
        } else {
            guiPanel.setBackground(new Color(0, 0, 0, 0));
        }
        guiFrame.setFont(new Font("Dialog", Font.PLAIN, 12));
        guiPanel.setFont(new Font("Dialog", Font.PLAIN, 12));
        GridBagLayout controlsgbl = new GridBagLayout();
        // *** controlsPanel contains the UI controls at the top of guiFrame
        controlsPanel = new JPanel(controlsgbl);
        controlsPanel.setBackground(new Color(211, 211, 211, 255));
        startStopButton = new JButton("Start");
        startStopButton.addActionListener(this);
        startStopButton.setBackground(Color.GREEN);
        continueButton = new JButton("Continue");
        continueButton.addActionListener(this);
        continueButton.setEnabled(false);
        screencapCheck = new JCheckBox("screen", bScreencap);
        screencapCheck.setBackground(controlsPanel.getBackground());
        screencapCheck.addActionListener(this);
        webcamCheck = new JCheckBox("camera", bWebcam);
        webcamCheck.setBackground(controlsPanel.getBackground());
        webcamCheck.addActionListener(this);
        audioCheck = new JCheckBox("audio", bAudio);
        audioCheck.setBackground(controlsPanel.getBackground());
        audioCheck.addActionListener(this);
        textCheck = new JCheckBox("text", bText);
        textCheck.setBackground(controlsPanel.getBackground());
        textCheck.addActionListener(this);
        JLabel fpsLabel = new JLabel("images/sec", SwingConstants.LEFT);
        fpsCB = new JComboBox<Double>(FPS_VALUES);
        int tempIndex = Arrays.asList(FPS_VALUES).indexOf(new Double(framesPerSec));
        fpsCB.setSelectedIndex(tempIndex);
        fpsCB.addActionListener(this);
        // The popup doesn't display over the transparent region;
        // therefore, just display a few rows to keep it within controlsPanel
        fpsCB.setMaximumRowCount(3);
        JLabel imgQualLabel = new JLabel("image qual", SwingConstants.LEFT);
        // The slider will use range 0 - 1000
        imgQualSlider = new JSlider(JSlider.HORIZONTAL, 0, 1000, (int) (imageQuality * 1000.0));
        // NOTE: The JSlider's initial width was too large, so I'd like to set its preferred size
        //       to try and constrain it some; also need to set its minimum size at the same time,
        //       or else when the user makes the GUI frame smaller, the JSlider would pop down to
        //       a really small minimum size.
        imgQualSlider.setPreferredSize(new Dimension(120, 30));
        imgQualSlider.setMinimumSize(new Dimension(120, 30));
        imgQualSlider.setBackground(controlsPanel.getBackground());
        includeMouseCursorCheck = new JCheckBox("Include mouse cursor in screen capture", bIncludeMouseCursor);
        includeMouseCursorCheck.setBackground(controlsPanel.getBackground());
        includeMouseCursorCheck.addActionListener(this);
        changeDetectCheck = new JCheckBox("Change detect", bChangeDetect);
        changeDetectCheck.setBackground(controlsPanel.getBackground());
        changeDetectCheck.addActionListener(this);
        fullScreenCheck = new JCheckBox("Full Screen", bFullScreen);
        fullScreenCheck.setBackground(controlsPanel.getBackground());
        fullScreenCheck.addActionListener(this);
        previewCheck = new JCheckBox("Preview", bPreview);
        previewCheck.setBackground(controlsPanel.getBackground());
        previewCheck.addActionListener(this);
        // Specify a small size for the text area, so that we can shrink down the UI w/o the scrollbars popping up
        textArea = new JTextArea(3, 10);
        // Add a Document listener to the JTextArea; this is how we will listen for changes to the document
        docChangeListener = new DocumentChangeListener(this);
        textArea.getDocument().addDocumentListener(docChangeListener);
        textScrollPane = new JScrollPane(textArea);
        // *** capturePanel
        capturePanel = new JPanel();
        if (!bShapedWindowSupportedI) {
            // Only make capturePanel translucent (ie, semi-transparent) if we aren't doing the Shaped window option
            capturePanel.setBackground(new Color(0, 0, 0, 16));
        } else {
            capturePanel.setBackground(new Color(0, 0, 0, 0));
        }
        capturePanel.setPreferredSize(new Dimension(500, 400));
        boolean bMacOS = false;
        String OS = System.getProperty("os.name", "generic").toLowerCase();
        if ((OS.indexOf("mac") >= 0) || (OS.indexOf("darwin") >= 0)) {
            bMacOS = true;
        }
        // Only have the CTstream UI stay on top of all other windows
        // if bStayOnTop is true (set by command line flag).  This is needed
        // for the Mac, because on that platform the user can't "reach through"
        // the capture frame to windows behind it.
        if (bStayOnTop) {
            guiFrame.setAlwaysOnTop(true);
        }

        //
        // Add components to the GUI
        //

        int row = 0;

        GridBagConstraints gbc = new GridBagConstraints();
        gbc.anchor = GridBagConstraints.WEST;
        gbc.fill = GridBagConstraints.NONE;
        gbc.weightx = 0;
        gbc.weighty = 0;

        //
        // First row: the controls panel
        //
        //  Add some extra horizontal padding around controlsPanel so the panel has some extra room
        // if we are running in web camera mode.
        gbc.insets = new Insets(0, 0, 0, 0);
        gbc.fill = GridBagConstraints.HORIZONTAL;
        gbc.weightx = 100;
        gbc.weighty = 0;
        Utility.add(guiPanel, controlsPanel, gbl, gbc, 0, row, 1, 1);
        gbc.insets = new Insets(0, 0, 0, 0);
        gbc.fill = GridBagConstraints.NONE;
        gbc.weightx = 0;
        gbc.weighty = 0;
        ++row;

        // Add controls to the controls panel
        int panelrow = 0;
        // (i) Start/Continue buttons
        GridBagLayout panelgbl = new GridBagLayout();
        JPanel subPanel = new JPanel(panelgbl);
        GridBagConstraints panelgbc = new GridBagConstraints();
        panelgbc.anchor = GridBagConstraints.WEST;
        panelgbc.fill = GridBagConstraints.NONE;
        panelgbc.weightx = 0;
        panelgbc.weighty = 0;
        subPanel.setBackground(controlsPanel.getBackground());
        panelgbc.insets = new Insets(0, 0, 0, 5);
        Utility.add(subPanel, startStopButton, panelgbl, panelgbc, 0, 0, 1, 1);
        panelgbc.insets = new Insets(0, 0, 0, 0);
        Utility.add(subPanel, continueButton, panelgbl, panelgbc, 1, 0, 1, 1);
        gbc.anchor = GridBagConstraints.CENTER;
        gbc.fill = GridBagConstraints.NONE;
        gbc.insets = new Insets(5, 0, 0, 0);
        Utility.add(controlsPanel, subPanel, controlsgbl, gbc, 0, panelrow, 1, 1);
        ++panelrow;
        gbc.anchor = GridBagConstraints.WEST;
        gbc.fill = GridBagConstraints.NONE;
        // (ii) select DataStreams to turn on
        panelgbl = new GridBagLayout();
        subPanel = new JPanel(panelgbl);
        panelgbc = new GridBagConstraints();
        panelgbc.anchor = GridBagConstraints.WEST;
        panelgbc.fill = GridBagConstraints.NONE;
        panelgbc.weightx = 0;
        panelgbc.weighty = 0;
        subPanel.setBackground(controlsPanel.getBackground());
        panelgbc.insets = new Insets(0, 0, 0, 0);
        Utility.add(subPanel, screencapCheck, panelgbl, panelgbc, 0, 0, 1, 1);
        Utility.add(subPanel, webcamCheck, panelgbl, panelgbc, 1, 0, 1, 1);
        Utility.add(subPanel, audioCheck, panelgbl, panelgbc, 2, 0, 1, 1);
        Utility.add(subPanel, textCheck, panelgbl, panelgbc, 3, 0, 1, 1);
        gbc.anchor = GridBagConstraints.CENTER;
        gbc.fill = GridBagConstraints.NONE;
        gbc.insets = new Insets(0, 0, 0, 0);
        Utility.add(controlsPanel, subPanel, controlsgbl, gbc, 0, panelrow, 1, 1);
        ++panelrow;
        // (iii) images/sec control
        panelgbl = new GridBagLayout();
        subPanel = new JPanel(panelgbl);
        panelgbc = new GridBagConstraints();
        panelgbc.anchor = GridBagConstraints.WEST;
        panelgbc.fill = GridBagConstraints.NONE;
        panelgbc.weightx = 0;
        panelgbc.weighty = 0;
        subPanel.setBackground(controlsPanel.getBackground());
        panelgbc.insets = new Insets(2, 0, 0, 0);
        Utility.add(subPanel, fpsLabel, panelgbl, panelgbc, 0, 0, 1, 1);
        panelgbc.insets = new Insets(2, 10, 0, 10);
        Utility.add(subPanel, fpsCB, panelgbl, panelgbc, 1, 0, 1, 1);
        gbc.insets = new Insets(0, 0, 0, 0);
        gbc.anchor = GridBagConstraints.CENTER;
        Utility.add(controlsPanel, subPanel, controlsgbl, gbc, 0, panelrow, 1, 1);
        ++panelrow;
        // (iv) image quality slider
        panelgbl = new GridBagLayout();
        subPanel = new JPanel(panelgbl);
        panelgbc = new GridBagConstraints();
        panelgbc.anchor = GridBagConstraints.WEST;
        panelgbc.fill = GridBagConstraints.NONE;
        panelgbc.weightx = 0;
        panelgbc.weighty = 0;
        subPanel.setBackground(controlsPanel.getBackground());
        JLabel sliderLabelLow = new JLabel("Low", SwingConstants.LEFT);
        JLabel sliderLabelHigh = new JLabel("High", SwingConstants.LEFT);
        panelgbc.insets = new Insets(-5, 0, 0, 0);
        Utility.add(subPanel, imgQualLabel, panelgbl, panelgbc, 0, 0, 1, 1);
        panelgbc.insets = new Insets(-5, 5, 0, 5);
        Utility.add(subPanel, sliderLabelLow, panelgbl, panelgbc, 1, 0, 1, 1);
        panelgbc.insets = new Insets(0, 0, 0, 0);
        Utility.add(subPanel, imgQualSlider, panelgbl, panelgbc, 2, 0, 1, 1);
        panelgbc.insets = new Insets(-5, 5, 0, 0);
        Utility.add(subPanel, sliderLabelHigh, panelgbl, panelgbc, 3, 0, 1, 1);
        gbc.insets = new Insets(0, 0, 0, 0);
        gbc.anchor = GridBagConstraints.CENTER;
        Utility.add(controlsPanel, subPanel, controlsgbl, gbc, 0, panelrow, 1, 1);
        ++panelrow;
        // (v) Include mouse cursor in screen capture image?
        gbc.anchor = GridBagConstraints.CENTER;
        gbc.fill = GridBagConstraints.NONE;
        gbc.weightx = 0;
        gbc.weighty = 0;
        gbc.insets = new Insets(0, 15, 5, 15);
        Utility.add(controlsPanel, includeMouseCursorCheck, controlsgbl, gbc, 0, panelrow, 1, 1);
        gbc.fill = GridBagConstraints.NONE;
        gbc.weightx = 0;
        gbc.weighty = 0;
        ++panelrow;
        // (vi) Change detect / Full screen / Preview checkboxes
        panelgbl = new GridBagLayout();
        subPanel = new JPanel(panelgbl);
        panelgbc = new GridBagConstraints();
        panelgbc.anchor = GridBagConstraints.WEST;
        panelgbc.fill = GridBagConstraints.NONE;
        panelgbc.weightx = 0;
        panelgbc.weighty = 0;
        subPanel.setBackground(controlsPanel.getBackground());
        panelgbc.insets = new Insets(0, 0, 0, 0);
        Utility.add(subPanel, changeDetectCheck, panelgbl, panelgbc, 0, 0, 1, 1);
        Utility.add(subPanel, fullScreenCheck, panelgbl, panelgbc, 1, 0, 1, 1);
        Utility.add(subPanel, previewCheck, panelgbl, panelgbc, 2, 0, 1, 1);
        gbc.anchor = GridBagConstraints.CENTER;
        gbc.fill = GridBagConstraints.NONE;
        gbc.insets = new Insets(-5, 0, 3, 0);
        Utility.add(controlsPanel, subPanel, controlsgbl, gbc, 0, panelrow, 1, 1);
        ++panelrow;
        // (vii) text field for the TextStream
        /*
        panelgbl = new GridBagLayout();
        subPanel = new JPanel(panelgbl);
        panelgbc = new GridBagConstraints();
        panelgbc.anchor = GridBagConstraints.CENTER;
        panelgbc.fill = GridBagConstraints.HORIZONTAL;
        panelgbc.weightx = 100;
        panelgbc.weighty = 100;
        subPanel.setBackground(controlsPanel.getBackground());
        panelgbc.insets = new Insets(0, 0, 0, 0);
        Utility.add(subPanel, textScrollPane, panelgbl, panelgbc, 1, 0, 1, 1);
        gbc.anchor = GridBagConstraints.CENTER;
        gbc.fill = GridBagConstraints.HORIZONTAL;
        gbc.weightx = 100;
        gbc.weighty = 100;
        gbc.insets = new Insets(0, 15, 3, 15);
        Utility.add(controlsPanel, subPanel, controlsgbl, gbc, 0, panelrow, 2, 1);
        ++panelrow;
        */
        gbc.anchor = GridBagConstraints.CENTER;
        gbc.fill = GridBagConstraints.HORIZONTAL;
        gbc.weightx = 100;
        gbc.weighty = 0;
        gbc.insets = new Insets(0, 15, 5, 15);
        Utility.add(controlsPanel, textScrollPane, controlsgbl, gbc, 0, panelrow, 1, 1);
        gbc.fill = GridBagConstraints.NONE;
        gbc.weightx = 0;
        gbc.weighty = 0;
        ++panelrow;

        //
        // Second row: the translucent/transparent capture panel
        //
        if (bShapedWindowSupportedI) {
            // Doing the Shaped window; set capturePanel inside guiPanel
            // a bit so the red from guiPanel shows at the edges
            gbc.insets = new Insets(5, 5, 5, 5);
        } else {
            // No shaped window; have capturePanel fill the area
            gbc.insets = new Insets(0, 0, 0, 0);
        }
        gbc.fill = GridBagConstraints.BOTH;
        gbc.weightx = 100;
        gbc.weighty = 100;
        Utility.add(guiPanel, capturePanel, gbl, gbc, 0, row, 1, 1);
        gbc.fill = GridBagConstraints.NONE;
        gbc.weightx = 0;
        gbc.weighty = 0;
        ++row;

        //
        // Add guiPanel to guiFrame
        //
        gbc.anchor = GridBagConstraints.CENTER;
        gbc.fill = GridBagConstraints.BOTH;
        gbc.weightx = 100;
        gbc.weighty = 100;
        gbc.insets = new Insets(0, 0, 0, 0);
        Utility.add(guiFrame, guiPanel, framegbl, gbc, 0, 0, 1, 1);

        //
        // Add menu
        //
        JMenuBar menuBar = createMenu();
        guiFrame.setJMenuBar(menuBar);

        //
        // If Shaped windows are supported, the region defined by capturePanel
        // will be "hollowed out" so that the user can reach through guiFrame
        // and interact with applications which are behind it.
        //
        // NOTE: This doesn't work on Mac OS (we've tried, but nothing seems
        //       to work to allow a user to reach through guiFrame to interact
        //       with windows behind).  May be a limitation or bug on Mac OS:
        //       https://bugs.openjdk.java.net/browse/JDK-8013450
        //
        if (bShapedWindowSupportedI) {
            guiFrame.addComponentListener(new ComponentAdapter() {
                // As the window is resized, the shape is recalculated here.
                @Override
                public void componentResized(ComponentEvent e) {
                    // Create a rectangle to cover the entire guiFrame
                    Area guiShape = new Area(new Rectangle(0, 0, guiFrame.getWidth(), guiFrame.getHeight()));
                    // Create another rectangle to define the hollowed out region of capturePanel
                    guiShape.subtract(new Area(new Rectangle(capturePanel.getX(), capturePanel.getY() + 23,
                            capturePanel.getWidth(), capturePanel.getHeight())));
                    guiFrame.setShape(guiShape);
                }
            });
        }

        //
        // Final guiFrame configuration details and displaying the GUI
        //
        guiFrame.pack();

        guiFrame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);

        guiFrame.addWindowListener(new WindowAdapter() {
            public void windowClosing(WindowEvent e) {
                exit(false);
            }
        });

        // Center on the screen
        guiFrame.setLocationRelativeTo(null);

        //
        // Set the taskbar/dock icon; note that Mac OS has its own way of doing it
        //
        if (bMacOS) {
            try {
                // JPW 2018/02/02: changed how to load images to work under Java 9
                InputStream imageInputStreamLarge = getClass().getClassLoader()
                        .getResourceAsStream("Icon_128x128.png");
                BufferedImage bufferedImageLarge = ImageIO.read(imageInputStreamLarge);
                /**
                 *
                 * Java 9 note: running the following code under Java 9 on a Mac will produce the following warning:
                 *
                 * WARNING: An illegal reflective access operation has occurred
                 * WARNING: Illegal reflective access by erigo.ctstream.CTstream (file:/Users/johnwilson/CT_versions/compiled_under_V8/CTstream.jar) to method com.apple.eawt.Application.getApplication()
                 * WARNING: Please consider reporting this to the maintainers of erigo.ctstream.CTstream
                 * WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
                 * WARNING: All illegal access operations will be denied in a future release
                 *
                 * This is because Java 9 has taken a step away from using reflection; see see the section titled
                 * "Illegal Access To Internal APIs" at https://blog.codefx.org/java/java-9-migration-guide/.
                 *
                 * A good fix (but only available in Java 9+) is to use the following:
                 *
                 *     java.awt.Taskbar taskbar = java.awt.Taskbar.getTaskbar();
                 *     taskbar.setIconImage(bufferedImageLarge);
                 *
                 * Could use reflection to make calls in class com.apple.eawt.Application; for example, see
                 * Bertil Chapuis' "dockicon.java" example code at https://gist.github.com/bchapuis/1562406
                 *
                 * For now, we just won't do dock icons under Mac OS.
                 *
                 **/
            } catch (Exception excepI) {
                System.err.println("Exception thrown trying to set icon: " + excepI);
            }
        } else {
            // The following has been tested under Windows 10 and Ubuntu 12.04 LTS
            try {
                // JPW 2018/02/02: changed how to load images to work under Java 9
                InputStream imageInputStreamLarge = getClass().getClassLoader()
                        .getResourceAsStream("Icon_128x128.png");
                BufferedImage bufferedImageLarge = ImageIO.read(imageInputStreamLarge);
                InputStream imageInputStreamMed = getClass().getClassLoader().getResourceAsStream("Icon_64x64.png");
                BufferedImage bufferedImageMed = ImageIO.read(imageInputStreamMed);
                InputStream imageInputStreamSmall = getClass().getClassLoader()
                        .getResourceAsStream("Icon_32x32.png");
                BufferedImage bufferedImageSmall = ImageIO.read(imageInputStreamSmall);
                List<BufferedImage> iconList = new ArrayList<BufferedImage>();
                iconList.add(bufferedImageLarge);
                iconList.add(bufferedImageMed);
                iconList.add(bufferedImageSmall);
                guiFrame.setIconImages(iconList);
            } catch (Exception excepI) {
                System.err.println("Exception thrown trying to set icon: " + excepI);
            }
        }

        ctSettings = new CTsettings(this, guiFrame);

        guiFrame.setVisible(true);

    }

    /**
     * Create menu for the GUI
     * 
     * @return  The new menu bar
     */
    private JMenuBar createMenu() {
        JMenuBar menuBar = new JMenuBar();
        JMenu menu = new JMenu("File");
        menuBar.add(menu);
        JMenuItem menuItem = new JMenuItem("Settings...");
        menu.add(menuItem);
        menuItem.addActionListener(this);
        menuItem = new JMenuItem("Launch CTweb server...");
        menu.add(menuItem);
        menuItem.addActionListener(this);
        menuItem = new JMenuItem("View data");
        menu.add(menuItem);
        menuItem.addActionListener(this);
        menuItem = new JMenuItem("CloudTurbine website");
        menu.add(menuItem);
        menuItem.addActionListener(this);
        menuItem = new JMenuItem("Exit");
        menu.add(menuItem);
        menuItem.addActionListener(this);
        return menuBar;
    }

    /**
     * Callback for menu items and "run time" controls in controlsPanel.
     * 
     * @param eventI The ActionEvent which has occurred.
     */
    @Override
    public void actionPerformed(ActionEvent eventI) {
        Object source = eventI.getSource();
        if (source == null) {
            return;
        } else if (source instanceof JComboBox) {
            JComboBox<?> fpsCB = (JComboBox<?>) source;
            framesPerSec = ((Double) fpsCB.getSelectedItem()).doubleValue();
            // Even when "change detect" is turned on, we always save an image at a rate which is
            // the slower of 1.0fps or the current frame rate; this is kind of a "key frame" of
            // sorts.  Because of this, the "change detect" checkbox is meaningless when images/sec
            // is 1.0 and lower.
            if (framesPerSec <= 1.0) {
                changeDetectCheck.setEnabled(false);
                // A nice side benefit of processing JCheckBox events using an Action listener
                // is that calling "changeDetectCheck.setSelected(false)" will NOT fire an
                // event; thus, we maintain our original value of bChangeDetect.
                changeDetectCheck.setSelected(false);
            } else {
                changeDetectCheck.setEnabled(true);
                changeDetectCheck.setSelected(bChangeDetect);
            }
        } else if ((source instanceof JCheckBox) && (((JCheckBox) source) == screencapCheck)) {
            bScreencap = screencapCheck.isSelected();
            if ((writeTask != null) && (writeTask.bIsRunning)) {
                if (bScreencap) {
                    startScreencapCapture();
                } else if (!bScreencap) {
                    stopScreencapCapture();
                }
            }
        } else if ((source instanceof JCheckBox) && (((JCheckBox) source) == webcamCheck)) {
            bWebcam = webcamCheck.isSelected();
            if ((writeTask != null) && (writeTask.bIsRunning)) {
                if (bWebcam) {
                    startWebcamCapture();
                } else if (!bWebcam) {
                    stopWebcamCapture();
                }
            }
        } else if ((source instanceof JCheckBox) && (((JCheckBox) source) == audioCheck)) {
            bAudio = audioCheck.isSelected();
            if ((writeTask != null) && (writeTask.bIsRunning)) {
                if (bAudio) {
                    startAudioCapture();
                } else if (!bAudio) {
                    stopAudioCapture();
                }
            }
        } else if ((source instanceof JCheckBox) && (((JCheckBox) source) == textCheck)) {
            bText = textCheck.isSelected();
            if ((writeTask != null) && (writeTask.bIsRunning)) {
                if (bText) {
                    startTextCapture();
                } else if (!bText) {
                    stopTextCapture();
                }
            }
        } else if ((source instanceof JCheckBox) && (((JCheckBox) source) == includeMouseCursorCheck)) {
            if (includeMouseCursorCheck.isSelected()) {
                bIncludeMouseCursor = true;
            } else {
                bIncludeMouseCursor = false;
            }
        } else if ((source instanceof JCheckBox) && (((JCheckBox) source) == changeDetectCheck)) {
            if (changeDetectCheck.isSelected()) {
                bChangeDetect = true;
            } else {
                bChangeDetect = false;
            }
        } else if ((source instanceof JCheckBox) && (((JCheckBox) source) == fullScreenCheck)) {
            if (fullScreenCheck.isSelected()) {
                bFullScreen = true;
                // Save the original height
                guiFrameOrigHeight = guiFrame.getHeight();
                // Shrink the GUI down to just controlsPanel
                Rectangle guiFrameBounds = guiFrame.getBounds();
                Rectangle updatedGUIFrameBounds = new Rectangle(guiFrameBounds.x, guiFrameBounds.y,
                        guiFrameBounds.width, controlsPanel.getHeight() + 22);
                guiFrame.setBounds(updatedGUIFrameBounds);
            } else {
                bFullScreen = false;
                // Expand the GUI to its original height
                Rectangle guiFrameBounds = guiFrame.getBounds();
                int updatedHeight = guiFrameOrigHeight;
                if (guiFrameOrigHeight == -1) {
                    updatedHeight = controlsPanel.getHeight() + 450;
                }
                Rectangle updatedGUIFrameBounds = new Rectangle(guiFrameBounds.x, guiFrameBounds.y,
                        guiFrameBounds.width, updatedHeight);
                guiFrame.setBounds(updatedGUIFrameBounds);
            }
            // To display or not display screencap preview is dependent on whether we are in full screen mode or not.
            if (screencapStream != null) {
                screencapStream.updatePreview();
            }
        } else if ((source instanceof JCheckBox) && (((JCheckBox) source) == previewCheck)) {
            if (previewCheck.isSelected()) {
                bPreview = true;
            } else {
                bPreview = false;
            }
        } else if (eventI.getActionCommand().equals("Start")) {
            // Make sure all needed values have been set
            String errStr = canCTrun();
            if (!errStr.isEmpty()) {
                JOptionPane.showMessageDialog(guiFrame, errStr, "CTstream settings error",
                        JOptionPane.ERROR_MESSAGE);
                return;
            }
            ((JButton) source).setText("Starting...");
            bContinueMode = false;
            firstCTtime = 0;
            continueWallclockInitTime = 0;
            startCapture();
            ((JButton) source).setText("Stop");
            ((JButton) source).setBackground(Color.RED);
            continueButton.setEnabled(false);
        } else if (eventI.getActionCommand().equals("Stop")) {
            ((JButton) source).setText("Stopping...");
            stopCapture();
            ((JButton) source).setText("Start");
            ((JButton) source).setBackground(Color.GREEN);
            continueButton.setEnabled(true);
        } else if (eventI.getActionCommand().equals("Continue")) {
            // This is just like "Start" except we pick up in time just where we left off
            bContinueMode = true;
            firstCTtime = 0;
            continueWallclockInitTime = 0;
            startCapture();
            startStopButton.setText("Stop");
            startStopButton.setBackground(Color.RED);
            continueButton.setEnabled(false);
        } else if (eventI.getActionCommand().equals("Settings...")) {
            boolean bBeenRunning = false;
            if (startStopButton.getText().equals("Stop")) {
                bBeenRunning = true;
            }
            // Stop capture (if it is running)
            stopCapture();
            startStopButton.setText("Start");
            startStopButton.setBackground(Color.GREEN);
            // Only want to enable the Continue button if the user had in fact been running
            if (bBeenRunning) {
                continueButton.setEnabled(true);
            }
            // Let user edit settings; the following function will not
            // return until the user clicks the OK or Cancel button.
            ctSettings.popupSettingsDialog();
        } else if (eventI.getActionCommand().equals("Launch CTweb server...")) {
            // Pop up dialog to launch the CTweb server
            new LaunchCTweb(this, guiFrame);
        } else if (eventI.getActionCommand().equals("CloudTurbine website")
                || eventI.getActionCommand().equals("View data")) {
            if (!Desktop.isDesktopSupported()) {
                System.err.println("\nNot able to launch URL in a browser window, feature not supported.");
                return;
            }
            Desktop desktop = Desktop.getDesktop();
            String urlStr = "http://cloudturbine.com";
            if (eventI.getActionCommand().equals("View data")) {
                // v=1 specifies to view 1 sec of data
                // y=4 specifies 4 grids in y-direction
                // n=X specifies the number of channels
                // Setup the channel list
                String chanListStr = "";
                int chanIdx = 0;
                NumberFormat formatter = new DecimalFormat("00");
                if (bWebcam) {
                    chanListStr = chanListStr + "&p" + formatter.format(chanIdx * 10) + "=" + sourceName + "/"
                            + webcamStreamName;
                    ++chanIdx;
                }
                if (bAudio) {
                    chanListStr = chanListStr + "&p" + formatter.format(chanIdx * 10) + "=" + sourceName + "/"
                            + audioStreamName;
                    ++chanIdx;
                }
                if (bScreencap) {
                    chanListStr = chanListStr + "&p" + formatter.format(chanIdx * 10) + "=" + sourceName + "/"
                            + screencapStreamName;
                    ++chanIdx;
                }
                if (bText) {
                    chanListStr = chanListStr + "&p" + formatter.format(chanIdx * 10) + "=" + sourceName + "/"
                            + textStreamName;
                    ++chanIdx;
                }
                if (chanIdx == 0) {
                    urlStr = "http://localhost:" + Integer.toString(webScanPort);
                } else {
                    urlStr = "http://localhost:" + Integer.toString(webScanPort)
                            + "/?dt=1000&c=0&f=false&sm=false&y=4&n=" + Integer.toString(chanIdx) + "&v=1"
                            + chanListStr;
                }
            }
            URI uri = null;
            try {
                uri = new URI(urlStr);
            } catch (URISyntaxException e) {
                System.err.println("\nURISyntaxException:\n" + e);
                return;
            }
            try {
                desktop.browse(uri);
            } catch (IOException e) {
                System.err.println("\nCaught IOException trying to go to " + urlStr + ":\n" + e);
            }
        } else if (eventI.getActionCommand().equals("Exit")) {
            exit(false);
        }
    }

    /**
     * Exit the application
     * 
     * @param bCalledFromShutdownHookI  Was this method called from the shutdown hook?
     */
    private void exit(boolean bCalledFromShutdownHookI) {
        stopCapture();
        System.err.println("\nExit CTstream\n");
        // If we *are* called from the shutdown hook, don't exit
        // (Java support for the shutdown hook must include its own
        //  code to call exit and if we call exit here, that gets
        //  screwed up.)
        if (!bCalledFromShutdownHookI) {
            // When we execute System.exit(0), the shutdown hook is called;
            // set bCallExitFromShutdownHook = false so that CTstream.exit()
            // isn't called from the shutdown hook.
            bCallExitFromShutdownHook = false;
            System.exit(0);
        }
    }

    /**
     * 
     * mouseDragged
     * 
     * Implement the mouseDragged method defined by interface MouseMotionListener.
     * 
     * This method implements the "guts" of our homemade window manager; this method
     * handles moving and resizing the frame.
     * 
     * Why have we implemented our own window manager?  Since translucent panels
     * can only be contained within undecorated Frames (see comments in the top
     * header above) and since undecorated Frames don't support moving/resizing,
     * we implement our own basic "window manager" by catching mouse move and drag
     * events.
     * 
     * @author John P. Wilson
     * @see java.awt.event.MouseMotionListener#mouseDragged(java.awt.event.MouseEvent)
     */
    @Override
    public void mouseDragged(MouseEvent mouseEventI) {
        // System.err.println("mouseDragged: " + mouseEventI.getX() + "," + mouseEventI.getY());
        // Keep the screen capture area to at least a minimum width and height
        boolean bDontMakeThinner = false;
        boolean bDontMakeShorter = false;
        if (capturePanel.getHeight() < 20) {
            bDontMakeShorter = true;
        }
        if (capturePanel.getWidth() < 20) {
            bDontMakeThinner = true;
        }

        Point currentPoint = mouseEventI.getLocationOnScreen();
        int currentPosX = currentPoint.x;
        int currentPosY = currentPoint.y;
        int deltaX = 0;
        int deltaY = 0;
        if ((mouseCommandMode != NO_COMMAND) && (mouseStartingPoint != null)) {
            deltaX = currentPosX - mouseStartingPoint.x;
            deltaY = currentPosY - mouseStartingPoint.y;
        }
        int oldFrameWidth = guiFrame.getBounds().width;
        int oldFrameHeight = guiFrame.getBounds().height;
        if (mouseCommandMode == MOVE_FRAME) {
            Point updatedGUIFrameLoc = new Point(frameStartingBounds.x + deltaX, frameStartingBounds.y + deltaY);
            guiFrame.setLocation(updatedGUIFrameLoc);
        } else if (mouseCommandMode == RESIZE_FRAME_NW) {
            int newFrameWidth = frameStartingBounds.width - deltaX;
            int newFrameHeight = frameStartingBounds.height - deltaY;
            if ((bDontMakeThinner && (newFrameWidth < oldFrameWidth))
                    || (bDontMakeShorter && (newFrameHeight < oldFrameHeight))) {
                return;
            }
            Rectangle updatedGUIFrameBounds = new Rectangle(frameStartingBounds.x + deltaX,
                    frameStartingBounds.y + deltaY, newFrameWidth, newFrameHeight);
            guiFrame.setBounds(updatedGUIFrameBounds);
        } else if (mouseCommandMode == RESIZE_FRAME_N) {
            int newFrameWidth = frameStartingBounds.width;
            int newFrameHeight = frameStartingBounds.height - deltaY;
            if (bDontMakeShorter && (newFrameHeight < oldFrameHeight)) {
                return;
            }
            Rectangle updatedGUIFrameBounds = new Rectangle(frameStartingBounds.x, frameStartingBounds.y + deltaY,
                    newFrameWidth, newFrameHeight);
            guiFrame.setBounds(updatedGUIFrameBounds);
        } else if (mouseCommandMode == RESIZE_FRAME_NE) {
            int newFrameWidth = frameStartingBounds.width + deltaX;
            int newFrameHeight = frameStartingBounds.height - deltaY;
            if ((bDontMakeThinner && (newFrameWidth < oldFrameWidth))
                    || (bDontMakeShorter && (newFrameHeight < oldFrameHeight))) {
                return;
            }
            Rectangle updatedGUIFrameBounds = new Rectangle(frameStartingBounds.x, frameStartingBounds.y + deltaY,
                    newFrameWidth, newFrameHeight);
            guiFrame.setBounds(updatedGUIFrameBounds);
        } else if (mouseCommandMode == RESIZE_FRAME_E) {
            int newFrameWidth = frameStartingBounds.width + deltaX;
            int newFrameHeight = frameStartingBounds.height;
            if (bDontMakeThinner && (newFrameWidth < oldFrameWidth)) {
                return;
            }
            Rectangle updatedGUIFrameBounds = new Rectangle(frameStartingBounds.x, frameStartingBounds.y,
                    newFrameWidth, newFrameHeight);
            guiFrame.setBounds(updatedGUIFrameBounds);
        } else if (mouseCommandMode == RESIZE_FRAME_SE) {
            int newFrameWidth = frameStartingBounds.width + deltaX;
            int newFrameHeight = frameStartingBounds.height + deltaY;
            if ((bDontMakeThinner && (newFrameWidth < oldFrameWidth))
                    || (bDontMakeShorter && (newFrameHeight < oldFrameHeight))) {
                return;
            }
            Rectangle updatedGUIFrameBounds = new Rectangle(frameStartingBounds.x, frameStartingBounds.y,
                    newFrameWidth, newFrameHeight);
            guiFrame.setBounds(updatedGUIFrameBounds);
        } else if (mouseCommandMode == RESIZE_FRAME_S) {
            int newFrameWidth = frameStartingBounds.width;
            int newFrameHeight = frameStartingBounds.height + deltaY;
            if (bDontMakeShorter && (newFrameHeight < oldFrameHeight)) {
                return;
            }
            Rectangle updatedGUIFrameBounds = new Rectangle(frameStartingBounds.x, frameStartingBounds.y,
                    newFrameWidth, newFrameHeight);
            guiFrame.setBounds(updatedGUIFrameBounds);
        } else if (mouseCommandMode == RESIZE_FRAME_SW) {
            int newFrameWidth = frameStartingBounds.width - deltaX;
            int newFrameHeight = frameStartingBounds.height + deltaY;
            if ((bDontMakeThinner && (newFrameWidth < oldFrameWidth))
                    || (bDontMakeShorter && (newFrameHeight < oldFrameHeight))) {
                return;
            }
            Rectangle updatedGUIFrameBounds = new Rectangle(frameStartingBounds.x + deltaX, frameStartingBounds.y,
                    newFrameWidth, newFrameHeight);
            guiFrame.setBounds(updatedGUIFrameBounds);
        } else if (mouseCommandMode == RESIZE_FRAME_W) {
            int newFrameWidth = frameStartingBounds.width - deltaX;
            int newFrameHeight = frameStartingBounds.height;
            if (bDontMakeThinner && (newFrameWidth < oldFrameWidth)) {
                return;
            }
            Rectangle updatedGUIFrameBounds = new Rectangle(frameStartingBounds.x + deltaX, frameStartingBounds.y,
                    newFrameWidth, newFrameHeight);
            guiFrame.setBounds(updatedGUIFrameBounds);
        } else {
            // See if we need to go into a particular command mode
            mouseStartingPoint = null;
            frameStartingBounds = null;
            mouseCommandMode = getGUIFrameCommandMode(mouseEventI.getPoint());
            if (mouseCommandMode != NO_COMMAND) {
                mouseStartingPoint = mouseEventI.getLocationOnScreen();
                frameStartingBounds = guiFrame.getBounds();
            }
        }
    }

    /**
     * 
     * mouseMoved
     * 
     * Implement the mouseMoved method defined by interface MouseMotionListener.
     * 
     * This method is part of our homemade window manager; specifically, this method
     * handles setting the appropriate mouse cursor based on where the user has
     * positioned the mouse on the JFrame window.
     * 
     * Why have we implemented our own window manager?  Since translucent panels
     * can only be contained within undecorated Frames (see comments in the top
     * header above) and since undecorated Frames don't support moving/resizing,
     * we implement our own basic "window manager" by catching mouse move and drag
     * events.
     * 
     * @author John P. Wilson
     * @see java.awt.event.MouseMotionListener#mouseMoved(java.awt.event.MouseEvent)
     */
    @Override
    public void mouseMoved(MouseEvent mouseEventI) {
        // System.err.println("mouseMoved: " + mouseEventI.getX() + "," + mouseEventI.getY());
        mouseCommandMode = NO_COMMAND;
        // Set mouse Cursor based on the current mouse position
        int commandMode = getGUIFrameCommandMode(mouseEventI.getPoint());
        switch (commandMode) {
        case NO_COMMAND:
            guiFrame.setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
            break;
        case MOVE_FRAME:
            guiFrame.setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
            break;
        case RESIZE_FRAME_NW:
            guiFrame.setCursor(new Cursor(Cursor.NW_RESIZE_CURSOR));
            break;
        case RESIZE_FRAME_N:
            guiFrame.setCursor(new Cursor(Cursor.N_RESIZE_CURSOR));
            break;
        case RESIZE_FRAME_NE:
            guiFrame.setCursor(new Cursor(Cursor.NE_RESIZE_CURSOR));
            break;
        case RESIZE_FRAME_E:
            guiFrame.setCursor(new Cursor(Cursor.E_RESIZE_CURSOR));
            break;
        case RESIZE_FRAME_SE:
            guiFrame.setCursor(new Cursor(Cursor.SE_RESIZE_CURSOR));
            break;
        case RESIZE_FRAME_S:
            guiFrame.setCursor(new Cursor(Cursor.S_RESIZE_CURSOR));
            break;
        case RESIZE_FRAME_SW:
            guiFrame.setCursor(new Cursor(Cursor.SW_RESIZE_CURSOR));
            break;
        case RESIZE_FRAME_W:
            guiFrame.setCursor(new Cursor(Cursor.W_RESIZE_CURSOR));
            break;
        default:
            guiFrame.setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
            break;
        }
    }

    /**
     * getGUIFrameCommandMode
     * 
     * Based on the given mouse position, determine what the corresponding mouse
     * "command mode" should be.  For instance, if the mouse is near the upper-
     * right corner of the window, this method will return RESIZE_FRAME_NE
     * because the mouse is in position to resize the frame from that corner.
     * 
     * Several offsets are defined in variables to specify if the cursor is
     * in a special region of the window for which moving or resizing can
     * take place:
     * 
     *     borderThickness:   a thickness all around the entire window, specifies
     *                    a "frame" around the window; if the mouse is within
     *                    this distance of the edge of the window, then this
     *                    method will return one of the following "resize"
     *                    commands, indicating that the mouse cursor is in
     *                    position to resize the JFrame along that edge:
     *                       RESIZE_FRAME_NW
     *                        RESIZE_FRAME_N
     *                        RESIZE_FRAME_NE
     *                        RESIZE_FRAME_E
     *                        RESIZE_FRAME_SE
     *                        RESIZE_FRAME_S
     *                        RESIZE_FRAME_SW
     *                        RESIZE_FRAME_W
     *      cornerActiveLength:   the accepted vertical or horizontal distance from
     *                     a window corner in order to be considered within
     *                     the "active" region of that corner; for example,
     *                     let's say the cursor is along the top edge of the
     *                     JFrame, 15 pixels from the upper right (NE) corner
     *                     and cornerActiveLength = 20; in this case, the
     *                     cursor is within the "active" area for that corner
     *                     and this method would return RESIZE_FRAME_NE;
     *                     if instead the cursor was 21 pixels from the corner
     *                     along the top edge of the JFrame, the cursor would
     *                     not be considered within the active area for the
     *                     corner but is instead in the active area for resizing
     *                     at the top of the window and this method will
     *                     return RESIZE_FRAME_N
     *      menubarHeight:      thickness at the top of the window which we
     *                     consider to be the menu bar region; if the mouse
     *                     is located within this region at the top of the
     *                     window but not within borderThickness of the very
     *                     edge of the JFrame, then this method will return
     *                     MOVE_FRAME, indicating that the mouse cursor is
     *                     in position to move the JFrame
     * 
     * @author John P. Wilson
     * @param  mousePosI  Mouse position
     */
    private int getGUIFrameCommandMode(Point mousePosI) {
        int borderThickness = 5;
        int cornerActiveLength = 20;
        int menubarHeight = 20;
        Rectangle frameBounds = guiFrame.getBounds();
        int frameWidth = frameBounds.width;
        int frameHeight = frameBounds.height;
        int cursorX = mousePosI.x;
        int cursorY = mousePosI.y;
        if ((cursorY <= cornerActiveLength) && ((cursorX <= borderThickness)
                || ((cursorY <= borderThickness) && (cursorX <= cornerActiveLength)))) {
            // Mouse is in the upper left corner of the window
            return RESIZE_FRAME_NW;
        } else if ((cursorY <= borderThickness) && (cursorX > cornerActiveLength)
                && (cursorX < (frameWidth - cornerActiveLength))) {
            // Mouse is near the top of the window
            return RESIZE_FRAME_N;
        } else if ((cursorY <= cornerActiveLength) && ((cursorX >= (frameWidth - borderThickness))
                || ((cursorY <= borderThickness) && (cursorX >= (frameWidth - cornerActiveLength))))) {
            // Mouse is in the upper right corner of the window
            return RESIZE_FRAME_NE;
        } else if ((cursorY <= menubarHeight) && (cursorX > cornerActiveLength)
                && (cursorX < (frameWidth - cornerActiveLength))) {
            // Mouse is in the top region of the window (in the menu bar region) but not near one of the corners
            return MOVE_FRAME;
        } else if ((cursorY > cornerActiveLength) && (cursorY < (frameHeight - cornerActiveLength))
                && (cursorX >= (frameWidth - borderThickness))) {
            // Mouse is on the right side of the window
            return RESIZE_FRAME_E;
        } else if ((cursorY >= (frameHeight - cornerActiveLength))
                && ((cursorX >= (frameWidth - borderThickness)) || ((cursorY >= (frameHeight - borderThickness))
                        && (cursorX >= (frameWidth - cornerActiveLength))))) {
            // Mouse is in the lower right corner of the window
            return RESIZE_FRAME_SE;
        } else if ((cursorY >= (frameHeight - borderThickness)) && (cursorX > cornerActiveLength)
                && (cursorX < (frameWidth - cornerActiveLength))) {
            // Mouse is near the bottom of the window
            return RESIZE_FRAME_S;
        } else if ((cursorY >= (frameHeight - cornerActiveLength)) && ((cursorX <= borderThickness)
                || ((cursorY >= (frameHeight - borderThickness)) && (cursorX <= cornerActiveLength)))) {
            // Mouse is in the lower left corner of the window
            return RESIZE_FRAME_SW;
        } else if ((cursorY > cornerActiveLength) && (cursorY < (frameHeight - cornerActiveLength))
                && (cursorX <= borderThickness)) {
            // Mouse is on the left side of the window
            return RESIZE_FRAME_W;
        }
        return NO_COMMAND;
    }

}