All Downloads are FREE. Search and download functionalities are using the official Maven repository.

marytts.client.MaryGUIClient Maven / Gradle / Ivy

The newest version!
/**
 * Copyright 2000-2009 DFKI GmbH.
 * All Rights Reserved.  Use is subject to license terms.
 *
 * This file is part of MARY TTS.
 *
 * MARY TTS is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, version 3 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see .
 *
 */
package marytts.client;

//General Java Classes
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.FocusTraversalPolicy;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.KeyboardFocusManager;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Vector;

import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.ImageIcon;
import javax.swing.JApplet;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextPane;
import javax.swing.KeyStroke;
import javax.swing.filechooser.FileFilter;
import javax.swing.text.BadLocationException;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyledDocument;

import marytts.client.http.MaryHttpClient;
import marytts.util.MaryUtils;
import marytts.util.http.Address;
import marytts.util.io.SimpleFileFilter;

import org.incava.util.diff.Diff;
import org.incava.util.diff.Difference;

/**
 * A GUI Interface to the Mary Client, allowing to access and modify intermediate processing results.
 * 
 * @author Marc.Schroeder, oytun.turk
 * @see MaryHttpClient The client implementation
 */

public class MaryGUIClient extends JPanel {

	/* -------------------- GUI stuff -------------------- */

	private Dimension paneDimension;
	// Input
	private JPanel inputTypePanel;
	private JComboBox cbInputType;
	private JPanel inputPanel;
	private JScrollPane inputScrollPane;
	private JTextPane inputText;
	private JPanel voicePanel;
	private JComboBox cbDefaultVoice;
	private JComboBox cbVoiceExampleText;
	private boolean doReplaceInput = true;
	// When the user changes input type, he is offered an example text for
	// the new input type. In order to prevent this when setting a new input
	// type from within the program, doReplaceInput must be set to false
	// before triggering the selection changed event.

	// Output
	private boolean showingTextOutput = true;
	private JPanel outputTypePanel;
	private JComboBox cbOutputType;
	private JButton bSaveOutput;
	private JTextPane outputText;
	private JScrollPane outputScrollPane;
	private JPanel audioPanel;
	private JButton bPlay;
	private JPanel savePanel;

	// Audio effects
	private boolean isButtonHide = true;
	private boolean showingAudioEffects = false;
	private JPanel showHidePanel;
	private JButton showHideEffects;
	private JList effectsList;
	private AudioEffectsBoxGUI effectsBox;
	private String[] effectNames;
	private String[] exampleParams;
	private String[] helpTexts;
	//

	// Processing Buttons
	private JPanel buttonPanel;
	private JButton bProcess;
	private JButton bEdit;
	private JButton bCompare;

	static JFrame mainFrame;
	static JApplet mainApplet;

	/* -------------------- Data and Processing stuff -------------------- */
	private MaryClient processor;

	private marytts.util.data.audio.AudioPlayer audioPlayer = null;
	private boolean allowSave;
	private boolean streamMp3 = false;
	private MaryClient.Voice prevVoice = null;

	// Map of limited Domain Voices and their example Texts
	private Map> limDomVoices = new HashMap>();

	private GridBagLayout gridBagLayout;
	private GridBagConstraints gridC;

	static FocusTraversalPolicy maryGUITraversal;

	/**
	 * Create a MaryGUIClient instance that connects to the server host and port as specified in the system properties
	 * "server.host" and "server.port", which default to "cling.dfki.uni-sb.de" and 59125, respectively.
	 * 
	 * @throws Exception
	 *             Exception
	 */
	public MaryGUIClient() throws Exception {
		super();
		// First the MaryHttpClient processor class, because it may provide
		// information needed in the GUI creation.
		try {
			processor = MaryClient.getMaryClient();
			streamMp3 = Boolean.getBoolean("stream.mp3");
		} catch (Exception e) {
			e.printStackTrace();
			JOptionPane.showMessageDialog(null, e.getMessage(), "Cannot connect to server", JOptionPane.ERROR_MESSAGE);
			System.exit(1);
		}
		allowSave = true;
		init();
	}

	/**
	 * Create a MaryGUIClient instance that connects to the given server host and port. This is meant to be used from Applets.
	 * 
	 * @param hostAddress
	 *            hostAddress
	 * @param applet
	 *            applet
	 * @throws IOException
	 *             IOException
	 */
	public MaryGUIClient(Address hostAddress, JApplet applet) throws IOException {
		super();
		// First the MaryHttpClient processor class, because it may provide
		// information needed in the GUI creation.
		try {
			processor = new MaryHttpClient(hostAddress, false, false);
		} catch (Exception e) {
			System.out.println("Problem creating mary client");
			e.printStackTrace();
			JOptionPane.showMessageDialog(null, e.getMessage(), "Cannot connect to server", JOptionPane.ERROR_MESSAGE);
			System.exit(1);
		}
		mainApplet = applet;
		allowSave = false;
		init();
	}

	/**
	 * Create an instance of the MaryHttpClient class which does the processing, and initialise the GUI.
	 * 
	 * @throws IOException
	 *             IOException
	 */
	public void init() throws IOException {
		maryGUITraversal = new MaryGUIFocusTraversalPolicy();
		// if this is a normal gui
		if (mainFrame != null) {
			mainFrame.setFocusTraversalPolicy(maryGUITraversal);
		} else { // this is an applet
			mainApplet.setFocusTraversalPolicy(maryGUITraversal);
		}

		paneDimension = new Dimension(250, 400);
		// Layout
		gridBagLayout = new GridBagLayout();
		gridC = new GridBagConstraints();

		gridC.insets = new Insets(2, 2, 2, 2);
		gridC.weightx = 0.1;
		gridC.weighty = 0.1;
		setLayout(gridBagLayout);

		// ////////////// Left Column: Input /////////////////////
		// Input type
		inputTypePanel = new JPanel();
		inputTypePanel.setLayout(new FlowLayout(FlowLayout.LEADING));
		gridC.gridx = 0;
		gridC.gridy = 0;
		gridC.gridwidth = 3;
		gridC.fill = GridBagConstraints.HORIZONTAL;
		gridBagLayout.setConstraints(inputTypePanel, gridC);
		add(inputTypePanel);
		gridC.gridwidth = 1;
		JLabel inputTypeLabel = new JLabel("Input Type: ");
		inputTypePanel.add(inputTypeLabel);
		assert processor.getInputDataTypes().size() > 0;
		assert processor.getOutputDataTypes().size() > 0;
		cbInputType = new JComboBox(processor.getInputDataTypes());
		cbInputType.setName("Input Type");
		cbInputType.getAccessibleContext().setAccessibleName("Input Type selection");
		cbInputType.setToolTipText("Specify the type of data contained " + "in the input text area below.");
		cbInputType.addItemListener(new ItemListener() {
			public void itemStateChanged(ItemEvent e) {
				if (e.getStateChange() == ItemEvent.SELECTED) {
					verifyExamplesVisible();
					if (doReplaceInput) {
						setExampleInputText();
					} else {
						// input text was set by other code
						doReplaceInput = true;
					}
					// setOutputTypeItems();
				}
			}
		});
		inputTypePanel.add(cbInputType);

		// Input Text area
		inputPanel = new JPanel();
		inputPanel.setLayout(new BoxLayout(inputPanel, BoxLayout.Y_AXIS));
		inputPanel.setMinimumSize(paneDimension);
		inputPanel.setPreferredSize(paneDimension);
		gridC.gridx = 0;
		gridC.gridy = 1;
		gridC.gridwidth = 3;
		gridC.gridheight = 3;
		gridC.weightx = 0.4;
		gridC.weighty = 0.8;
		// gridC.ipadx = 270;
		// gridC.ipady = 200;
		gridC.fill = GridBagConstraints.BOTH;
		gridBagLayout.setConstraints(inputPanel, gridC);
		add(inputPanel);
		gridC.gridwidth = 1;
		gridC.gridheight = 1;
		gridC.weightx = 0.1;
		gridC.weighty = 0.1;
		gridC.ipadx = 0;
		gridC.ipady = 0;
		gridC.fill = GridBagConstraints.NONE;
		inputText = new JTextPane();

		inputText.getAccessibleContext().setAccessibleName("Input Text Area");

		// Set Tab and Shift-Tab for Keyboard movement
		Set forwardKeys = new HashSet();
		forwardKeys.add(KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0, false));
		inputText.setFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, forwardKeys);
		Set backwardKeys = new HashSet();
		backwardKeys.add(KeyStroke.getKeyStroke(KeyEvent.VK_TAB, KeyEvent.SHIFT_MASK + KeyEvent.SHIFT_DOWN_MASK, false));
		inputText.setFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, backwardKeys);

		inputScrollPane = new JScrollPane(inputText);
		inputPanel.add(inputScrollPane);
		inputScrollPane.setPreferredSize(new Dimension(inputPanel.getPreferredSize().width, 1000));
		// example text for limDom voices
		cbVoiceExampleText = new JComboBox();
		cbVoiceExampleText.setName("Example Text");
		cbVoiceExampleText.getAccessibleContext().setAccessibleName("Example text selection");
		cbVoiceExampleText.addItemListener(new ItemListener() {
			public void itemStateChanged(ItemEvent e) {
				if (e.getStateChange() == ItemEvent.SELECTED) {
					if (doReplaceInput && ((MaryClient.DataType) cbInputType.getSelectedItem()).name().startsWith("TEXT"))
						setInputText((String) cbVoiceExampleText.getSelectedItem());
				}
			}
		});
		cbVoiceExampleText.setPreferredSize(new Dimension(inputPanel.getPreferredSize().width, 25));
		inputPanel.add(cbVoiceExampleText);

		// Select voice
		voicePanel = new JPanel();
		voicePanel.setLayout(new FlowLayout(FlowLayout.LEADING));
		gridC.gridx = 0;
		gridC.gridy = 4;
		gridC.gridwidth = 4;
		gridC.gridheight = 2;
		gridC.fill = GridBagConstraints.HORIZONTAL;
		gridBagLayout.setConstraints(voicePanel, gridC);
		add(voicePanel);
		gridC.gridwidth = 1;
		gridC.gridheight = 1;
		JLabel voiceLabel = new JLabel("Voice:");
		voicePanel.add(voiceLabel);
		cbDefaultVoice = new JComboBox();
		cbDefaultVoice.setName("Voice selection");
		cbDefaultVoice.getAccessibleContext().setAccessibleName("Voice selection");
		voicePanel.add(cbDefaultVoice);
		cbDefaultVoice.addItemListener(new ItemListener() {
			public void itemStateChanged(ItemEvent e) {
				if (e.getStateChange() == ItemEvent.SELECTED) {
					fillExampleTexts();
					verifyExamplesVisible();
					MaryClient.Voice voice = (MaryClient.Voice) cbDefaultVoice.getSelectedItem();
					MaryClient.DataType dataType = (MaryClient.DataType) cbInputType.getSelectedItem();
					if (doReplaceInput
							&& (voice.isLimitedDomain() && dataType.name().startsWith("TEXT") || getPrevVoice() == null || !getPrevVoice()
									.getLocale().equals(voice.getLocale())))
						setExampleInputText();
					setPrevVoice(voice);

					updateAudioEffects();
				}
			}
		});

		// For the limited domain voices, get example texts:
		Vector voices = processor.getVoices();
		if (voices != null) {
			for (MaryClient.Voice v : voices) {
				if (v.isLimitedDomain()) {
					limDomVoices.put(v.name(), processor.getVoiceExampleTextsLimitedDomain(v.name()));
				}
			}
		}

		verifyDefaultVoices();
		fillExampleTexts();
		verifyExamplesVisible();
		setExampleInputText();

		// ////////////// Centre Column: Buttons /////////////////////
		// Action buttons in centre
		buttonPanel = new JPanel();
		buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.Y_AXIS));
		gridC.gridx = 3;
		gridC.gridy = 1;
		gridC.gridheight = 3;
		gridC.fill = GridBagConstraints.BOTH;
		gridBagLayout.setConstraints(buttonPanel, gridC);
		add(buttonPanel);
		bProcess = new JButton("Process ->");
		bProcess.setToolTipText("Call the Mary Server." + "The input will be transformed into the specified output type.");
		bProcess.getAccessibleContext().setAccessibleName("Process button");
		bProcess.setActionCommand("process");
		bProcess.setMnemonic('P');
		bProcess.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				processInput();
				verifyEnableButtons();
			}
		});
		buttonPanel.add(Box.createVerticalGlue());
		buttonPanel.add(bProcess);
		bProcess.setAlignmentX(Component.CENTER_ALIGNMENT);
		buttonPanel.add(Box.createVerticalGlue());

		bEdit = new JButton("<- Edit");
		bEdit.setToolTipText("Edit the content of the output text area as the new input."
				+ " The current content of the input text area will be discarded.");
		bEdit.getAccessibleContext().setAccessibleName("Edit button");
		bEdit.setActionCommand("edit");
		bEdit.setMnemonic('E');
		bEdit.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				editOutput();
				verifyEnableButtons();
			}
		});
		buttonPanel.add(bEdit);
		bEdit.setAlignmentX(Component.CENTER_ALIGNMENT);
		buttonPanel.add(Box.createVerticalGlue());

		bCompare = new JButton("<- Compare ->");
		bCompare.setToolTipText("Compare input and output" + "(available only if both are MaryXML types).");
		bCompare.getAccessibleContext().setAccessibleName("Compare button");
		bCompare.setActionCommand("compare");
		bCompare.setMnemonic('C');
		bCompare.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				compareTexts();
				verifyEnableButtons();
			}
		});
		buttonPanel.add(bCompare);
		bCompare.setAlignmentX(Component.CENTER_ALIGNMENT);
		buttonPanel.add(Box.createVerticalGlue());
		buttonPanel.setPreferredSize(new Dimension(buttonPanel.getPreferredSize().width, paneDimension.height));

		// ////////////// Right Column: Output /////////////////////
		// Output type
		outputTypePanel = new JPanel();
		outputTypePanel.setLayout(new FlowLayout(FlowLayout.TRAILING));
		gridC.gridx = 4;
		gridC.gridy = 0;
		gridC.gridwidth = 3;
		gridC.gridheight = 1;
		gridC.ipady = 10;
		gridC.fill = GridBagConstraints.HORIZONTAL;
		gridBagLayout.setConstraints(outputTypePanel, gridC);
		add(outputTypePanel);
		gridC.ipady = 0;
		gridC.gridwidth = 1;
		JLabel outputTypeLabel = new JLabel("Output Type: ");
		outputTypePanel.add(outputTypeLabel);
		cbOutputType = new JComboBox();
		cbOutputType.setName("Output type");
		cbOutputType.getAccessibleContext().setAccessibleName("Output type selection");
		setOutputTypeItems();
		// The last possible output type (= audio) is the default
		// output type:
		cbOutputType.setSelectedIndex(cbOutputType.getItemCount() - 1);
		cbOutputType.setToolTipText("Specify the output type for the next " + "processing action (Process button).");
		cbOutputType.addItemListener(new ItemListener() {
			public void itemStateChanged(ItemEvent e) {
				if (e.getStateChange() == ItemEvent.SELECTED) {
					verifyOutputDisplay();
					verifyEnableButtons();
					revalidate();
				}
			}
		});
		outputTypePanel.add(cbOutputType);

		// Output Text area
		if (((MaryClient.DataType) cbOutputType.getSelectedItem()).isTextType())
			showingTextOutput = true;
		else
			showingTextOutput = false;
		outputText = new JTextPane();
		outputText.getAccessibleContext().setAccessibleName("Output text");

		// set tab and shift-tab for keyboard movement
		outputText.setFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, forwardKeys);
		outputText.setFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, backwardKeys);

		// outputText.setLineWrap(false);
		outputText.setEditable(false);
		outputScrollPane = new JScrollPane(outputText);
		outputScrollPane.setMinimumSize(paneDimension);
		outputScrollPane.setPreferredSize(paneDimension);
		gridC.gridx = 4;
		gridC.gridy = 1;
		gridC.gridwidth = 3;
		gridC.gridheight = 3;
		gridC.weightx = 0.4;
		gridC.weighty = 0.8;
		gridC.fill = GridBagConstraints.BOTH;
		gridBagLayout.setConstraints(outputScrollPane, gridC);
		if (!showingTextOutput)
			outputScrollPane.setVisible(false);
		add(outputScrollPane);

		// Audio effects
		setAudioEffects();

		isButtonHide = false;
		if (effectsBox.hasEffects()) {
			gridC.gridx = 4;
			gridC.gridy = 1;
			gridC.gridwidth = 3;
			gridC.gridheight = 3;
			gridC.weightx = 0.1;
			gridC.weighty = 0.1;
			gridC.ipadx = 0;
			gridC.ipady = 0;
			gridC.fill = GridBagConstraints.BOTH;
			gridBagLayout.setConstraints(effectsBox.mainPanel, gridC);

			add(effectsBox.mainPanel);

			updateAudioEffects();

			if (effectsBox != null && effectsBox.mainPanel != null) {
				showHidePanel = new JPanel();
				showHidePanel.setPreferredSize(paneDimension);
				showHidePanel.setLayout(new BoxLayout(showHidePanel, BoxLayout.Y_AXIS));

				if (!showingTextOutput) {
					showHideEffects = new JButton("Hide Effects");
					isButtonHide = true;
				} else {
					showHideEffects = new JButton("Show Effects");
					isButtonHide = false;
				}

				showHideEffects.setToolTipText("Hide or show available audio effects for post-processing the TTS output");
				showHideEffects.getAccessibleContext().setAccessibleName("Hide/Show audio effects button");

				showHideEffects.addActionListener(new ActionListener() {
					public void actionPerformed(ActionEvent e) {
						showHideEffectAction();
					}
				});

				showHidePanel.add(Box.createVerticalGlue());
				showHidePanel.add(showHideEffects);
				showHidePanel.add(Box.createVerticalGlue());
				showHideEffects.setAlignmentX(Component.CENTER_ALIGNMENT);

				gridC.gridx = 4;
				if (!showingTextOutput)
					gridC.gridy = 4;
				else
					gridC.gridy = 3;

				gridC.gridwidth = 3;
				gridC.gridheight = 1;
				gridC.ipady = 10;
				gridC.fill = GridBagConstraints.BOTH;
				gridBagLayout.setConstraints(showHidePanel, gridC);
				add(showHidePanel);
				gridC.ipady = 0;

				if (effectsBox.mainPanel != null) {
					if (!showingTextOutput && isButtonHide) {
						effectsBox.mainPanel.setVisible(true);
						showingAudioEffects = true;
					} else {
						effectsBox.mainPanel.setVisible(false);
						showingAudioEffects = false;
					}
				}
			}
		}
		//

		gridC.gridwidth = 1;
		gridC.gridheight = 1;
		gridC.weightx = 0.1;
		gridC.weighty = 0.1;
		gridC.ipadx = 0;
		gridC.ipady = 0;
		gridC.fill = GridBagConstraints.NONE;

		// Overlapping location: Audio play button
		audioPanel = new JPanel();
		audioPanel.setPreferredSize(paneDimension);
		audioPanel.setLayout(new BoxLayout(audioPanel, BoxLayout.Y_AXIS));
		bPlay = new JButton("Play");
		bPlay.setToolTipText("Synthesize and play the resulting audio stream.");
		bPlay.getAccessibleContext().setAccessibleName("Play button");
		bPlay.setActionCommand("play");
		bPlay.setMnemonic('P');
		bPlay.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				if (audioPlayer != null) { // an audioPlayer is currently playing
					audioPlayer.cancel();
					audioPlayer = null;
					bPlay.setText("Play");
				} else {
					processInput();
				}
			}
		});
		audioPanel.add(Box.createVerticalGlue());
		audioPanel.add(bPlay);
		audioPanel.add(Box.createVerticalGlue());
		bPlay.setAlignmentX(Component.CENTER_ALIGNMENT);
		// bPlay.setMaximumSize(bPlay.getPreferredSize());

		if (showingTextOutput)
			audioPanel.setVisible(false);
		gridC.gridx = 4;
		if (showingAudioEffects)
			gridC.gridy = 5;
		else
			gridC.gridy = 4;
		gridC.gridwidth = 3;
		gridC.gridheight = 1;
		gridC.ipady = 10;
		gridC.fill = GridBagConstraints.BOTH;
		gridBagLayout.setConstraints(audioPanel, gridC);
		add(audioPanel);
		gridC.gridwidth = 1;
		gridC.ipady = 0;

		// Output Save button
		if (allowSave) {
			savePanel = new JPanel();
			savePanel.setLayout(new FlowLayout(FlowLayout.TRAILING));
			gridC.gridx = 4;
			gridC.ipady = 10;
			if (showingAudioEffects)
				gridC.gridy = 6;
			else
				gridC.gridy = 5;

			gridC.fill = GridBagConstraints.HORIZONTAL;
			gridBagLayout.setConstraints(savePanel, gridC);
			add(savePanel);
			ImageIcon saveIcon = new ImageIcon("save.gif");
			bSaveOutput = new JButton("Save...", saveIcon);
			bSaveOutput.setToolTipText("Save the output as a file.");
			bSaveOutput.getAccessibleContext().setAccessibleName("Save Output button");
			bSaveOutput.setActionCommand("saveOutput");
			bSaveOutput.setMnemonic('S');
			bSaveOutput.addActionListener(new ActionListener() {
				public void actionPerformed(ActionEvent e) {
					try {
						saveOutput();
					} catch (IOException e1) {
						// TODO Auto-generated catch block
						e1.printStackTrace();
					} catch (InterruptedException e1) {
						// TODO Auto-generated catch block
						e1.printStackTrace();
					}
				}
			});
			savePanel.add(bSaveOutput);
			gridC.ipady = 0;
		}
		setPreferredSize(new Dimension(720, 480));

		verifyEnableButtons();
		cbInputType.requestFocusInWindow();

		showHideEffectAction();
	}

	private void setAudioEffects() {
		String availableAudioEffects = "";
		String strLineBreak = "";

		try {
			availableAudioEffects = processor.getAudioEffects();
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

		effectsBox = new AudioEffectsBoxGUI(availableAudioEffects);
		// TODO: this is overkill, we could load a help text when the
		// user presses a help button, but it would require re-engineering the code a bit.
		// fill help through separate queries:
		for (int i = 0; i < effectsBox.getData().getTotalEffects(); i++) {
			AudioEffectControlData eff = effectsBox.getData().getControlData(i);
			try {
				eff.setHelpText(processor.requestEffectHelpText(eff.getEffectName()));
			} catch (IOException e) {
				e.printStackTrace();
			}
		}

	}

	private void showHideEffectAction() {
		if (isButtonHide) {
			if (effectsBox != null && effectsBox.mainPanel != null)
				effectsBox.mainPanel.setVisible(false);

			if (showHideEffects != null)
				showHideEffects.setText("Show Effects");

			isButtonHide = false;
			showingAudioEffects = false;
		} else {
			if (effectsBox != null && effectsBox.mainPanel != null)
				effectsBox.mainPanel.setVisible(true);

			if (showHideEffects != null)
				showHideEffects.setText("Hide Effects");

			isButtonHide = true;
			showingAudioEffects = true;
		}
	}

	// If there are any effects available for the selected voice
	// update audio effects box accordingly
	private void updateAudioEffects() {
		// Overlapping location: Audio effects box
		// Initialize the effects here (normally using info from the server)
		if (effectsBox != null) {
			if (effectsBox.hasEffects() && effectsBox.getData().getTotalEffects() > 0) {
				MaryClient.Voice voice = (MaryClient.Voice) cbDefaultVoice.getSelectedItem();
				if (voice == null)
					return;

				for (int i = 0; i < effectsBox.getData().getTotalEffects(); i++) {
					String effectName = effectsBox.getData().getControlData(i).getEffectName();

					effectsBox.effectControls[i].setVisible(true);
					// Do not display audio effects that are only available for the HMM voice
					if (!voice.isHMMVoice()) {
						try {
							if (processor.isHMMEffect(effectName))
								effectsBox.effectControls[i].setVisible(false);

						} catch (Exception e) {
							// TODO Auto-generated catch block
							e.printStackTrace();
						}
					}
				}

				effectsBox.show();
			}
		}
		//
	}

	/**
	 * Create a single String parameter by reading the selected effect parameters in the interface If no effect is selected or the
	 * effects are not being shown, an empty String is returned.
	 * 
	 * @return a String of the form
	 *         "Effect1Name(Effect1Parameter1=Effect1Value1; Effect1Parameter2=Effect1Value2)+Effect2Name(Effect2Parameter1=Effect2Value1)"
	 *         For example, "Robot(amount=100)+Whisper(amount=50)" will convert the output into a whispered robotic voice with the
	 *         specified amounts.
	 */
	private String getAudioEffectsString() {
		StringBuilder effects = new StringBuilder();
		String key, value;
		if (effectsBox != null && isButtonHide) {
			for (int i = 0; i < effectsBox.getData().getTotalEffects(); i++) {
				if (effectsBox.effectControls[i].chkEnabled.isSelected()) {
					String name = effectsBox.getData().getControlData(i).getEffectName();
					String params = effectsBox.effectControls[i].txtParams.getText().trim();
					if (effects.length() > 0)
						effects.append("+");
					effects.append(name);
					if (params != null && params.length() > 0) {
						effects.append("(").append(params).append(")");
					}
				}
			}
		}
		return effects.toString();
	}

	private void setExampleInputText() {
		MaryClient.Voice defaultVoice = (MaryClient.Voice) cbDefaultVoice.getSelectedItem();
		if (defaultVoice == null)
			return;
		MaryClient.DataType inputType = (MaryClient.DataType) cbInputType.getSelectedItem();
		if (defaultVoice.isLimitedDomain() && inputType.name().startsWith("TEXT")) {
			setInputText((String) cbVoiceExampleText.getSelectedItem());
		} else {
			try {
				String exampleText = processor.getServerExampleText(inputType.name(), defaultVoice.getLocale().toString());
				setInputText(exampleText);
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}

	private void fillExampleTexts() {
		MaryClient.Voice defaultVoice = (MaryClient.Voice) cbDefaultVoice.getSelectedItem();
		if (defaultVoice == null || !defaultVoice.isLimitedDomain())
			return;
		Vector sentences = (Vector) limDomVoices.get(defaultVoice.name());
		assert sentences != null;
		cbVoiceExampleText.removeAllItems();
		for (int i = 0; i < sentences.size(); i++) {
			cbVoiceExampleText.addItem(sentences.get(i));
		}
		cbVoiceExampleText.setSelectedIndex(0);
	}

	private void verifyExamplesVisible() {
		MaryClient.Voice defaultVoice = (MaryClient.Voice) cbDefaultVoice.getSelectedItem();
		MaryClient.DataType inputType = (MaryClient.DataType) cbInputType.getSelectedItem();

		if (defaultVoice != null && defaultVoice.isLimitedDomain() && inputType.name().startsWith("TEXT")) {
			cbVoiceExampleText.setVisible(true);
		} else {
			cbVoiceExampleText.setVisible(false);
		}
	}

	private void verifyEnableButtons() {
		if (((MaryClient.DataType) cbOutputType.getSelectedItem()).isTextType()) {
			buttonPanel.setVisible(true);
			if (!cbOutputType.hasFocus())
				bProcess.requestFocusInWindow();
		} else { // do not show these three buttons for audio output:
			buttonPanel.setVisible(false);
			if (!cbOutputType.hasFocus())
				bPlay.requestFocusInWindow();
		}
		// Edit button:
		if (showingTextOutput) {
			if (outputText.getText().length() == 0) {
				if (allowSave)
					bSaveOutput.setEnabled(false);
				bEdit.setEnabled(false);
			} else {
				if (allowSave)
					bSaveOutput.setEnabled(true);
				bEdit.setEnabled(true);
			}
		} else { // audio output
			if (allowSave)
				bSaveOutput.setEnabled(true);
		}
		// Compare button:
		// Only enabled if both input and output are text types
		if (((MaryClient.DataType) cbOutputType.getSelectedItem()).isTextType() && outputText.getText().length() > 0) {
			bCompare.setEnabled(true);
		} else {
			bCompare.setEnabled(false);
		}
	}

	/**
	 * Verify that the list of voices in cbDefaultVoices matches the language of the input format.
	 * 
	 * @throws IOException
	 *             IOException
	 */
	private void verifyDefaultVoices() throws IOException {
		MaryClient.DataType inputType = (MaryClient.DataType) cbInputType.getSelectedItem();
		// Is the default voice still suitable for the input locale?
		MaryClient.Voice defaultVoice = (MaryClient.Voice) cbDefaultVoice.getSelectedItem();
		// Reset the list, just in case
		cbDefaultVoice.removeAllItems();
		Vector voices = processor.getVoices();
		if (voices != null) {
			for (MaryClient.Voice v : processor.getVoices()) {
				cbDefaultVoice.addItem(v);
			}
			if (defaultVoice != null) {
				cbDefaultVoice.setSelectedItem(defaultVoice);
			} else { // First in list is default voice:
				cbDefaultVoice.setSelectedIndex(0);
			}
		}
	}

	private void setOutputTypeItems() throws IOException {
		MaryClient.DataType inputType = (MaryClient.DataType) cbInputType.getSelectedItem();
		MaryClient.DataType selectedItem = (MaryClient.DataType) cbOutputType.getSelectedItem();
		cbOutputType.removeAllItems();
		for (MaryClient.DataType d : processor.getOutputDataTypes()) {
			cbOutputType.addItem(d);
		}
		cbOutputType.setSelectedItem(selectedItem);
	}

	private void verifyOutputDisplay() {
		if (((MaryClient.DataType) cbOutputType.getSelectedItem()).isTextType()) {
			setOutputText(""); // erase the output text
			if (!showingTextOutput) {
				// showing Audio Output
				// need to change output display
				audioPanel.setVisible(false);

				if (effectsBox != null) {
					if (effectsBox.mainPanel != null)
						effectsBox.mainPanel.setVisible(false);

					if (showHidePanel != null)
						showHidePanel.setVisible(false);
				}

				outputScrollPane.setVisible(true);
				showingTextOutput = true;
				revalidate();
			}
		} else { // Audio output
			if (showingTextOutput) {
				// change output display
				outputScrollPane.setVisible(false);
				audioPanel.setVisible(true);

				if (effectsBox != null && effectsBox.mainPanel != null) {
					if (showHidePanel != null)
						showHidePanel.setVisible(true);

					if (effectsBox.mainPanel != null && isButtonHide)
						effectsBox.mainPanel.setVisible(true);
				}

				showingTextOutput = false;
				revalidate();
			}
		}
	}

	/* -------------------- Processing callers -------------------- */
	private File lastDirectory = null;
	private String lastExtension = null;

	private void saveOutput() throws IOException, InterruptedException {
		if (!allowSave)
			return;
		try {
			if (showingTextOutput) {
				JFileChooser fc = new JFileChooser();
				if (lastDirectory != null) {
					fc.setCurrentDirectory(lastDirectory);
				}
				int returnVal = fc.showSaveDialog(this);
				if (returnVal == JFileChooser.APPROVE_OPTION) {
					File saveFile = fc.getSelectedFile();
					lastDirectory = saveFile.getParentFile();
					PrintWriter w = new PrintWriter(new FileWriter(saveFile));
					w.print(outputText.getText());
					w.close();
				}
			} else { // audio data
				JFileChooser fc = new JFileChooser();
				if (lastDirectory != null) {
					fc.setCurrentDirectory(lastDirectory);
				}
				Vector knownAudioTypes = processor.getAudioFileFormatTypes();
				String[] extensions = new String[knownAudioTypes.size()];
				String[] typeNames = new String[knownAudioTypes.size()];
				FileFilter defaultFilter = null;
				for (int i = 0; i < knownAudioTypes.size(); i++) {
					int iSpace = knownAudioTypes.get(i).indexOf(' ');
					typeNames[i] = knownAudioTypes.get(i).substring(0, iSpace);
					extensions[i] = knownAudioTypes.get(i).substring(iSpace + 1);
					FileFilter ff = new SimpleFileFilter(extensions[i], typeNames[i] + " (." + extensions[i] + ")");
					fc.addChoosableFileFilter(ff);
					if (lastExtension != null && lastExtension.equals(extensions[i])) {
						defaultFilter = ff;
					}
					if (defaultFilter != null) {
						fc.setFileFilter(defaultFilter);
					}
				}
				int returnVal = fc.showSaveDialog(this);
				if (returnVal == JFileChooser.APPROVE_OPTION) {
					File saveFile = fc.getSelectedFile();
					String ext = MaryUtils.getExtension(saveFile);
					if (ext == null) { // no extension in the file name, append from filefilter
						ext = ((SimpleFileFilter) fc.getFileFilter()).getExtension();
						saveFile = new File(saveFile.getAbsolutePath() + "." + ext);
					}
					lastDirectory = saveFile.getParentFile();
					lastExtension = ext;
					String audioType = null;
					for (int i = 0; i < knownAudioTypes.size(); i++) {
						if (extensions[i].equals(ext)) {
							audioType = typeNames[i];
							break;
						}
					}
					if (audioType == null) { // file has unknown extension
						showErrorMessage("Unknown audio type", "Cannot write file of type `." + ext + "'");
					} else { // OK, we know what to do
						FileOutputStream fos = new FileOutputStream(saveFile);
						processor.process(inputText.getText(), ((MaryClient.DataType) cbInputType.getSelectedItem()).name(),
								"AUDIO", ((MaryClient.Voice) cbDefaultVoice.getSelectedItem()).getLocale().toString(), audioType,
								((MaryClient.Voice) cbDefaultVoice.getSelectedItem()).name(), "", getAudioEffectsString(), null,
								fos);
						fos.close();
					}
				}
			}
		} catch (IOException e) {
			e.printStackTrace();
			showErrorMessage("IOException", e.getMessage());
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	private void makeTextPlain(StyledDocument doc) {
		SimpleAttributeSet emptyAttributes = new SimpleAttributeSet();
		doc.setCharacterAttributes(0, doc.getLength(), emptyAttributes, true);
	}

	/**
	 * Set everything that is not between < and > to bold. This is "dumb", i.e. it will not try to analyse the contents of tags,
	 * and fail at situations like , where i would be printed in bold as well.
	 * 
	 * @param doc
	 *            doc
	 */
	private void highlightText(StyledDocument doc) {
		SimpleAttributeSet highlighted = new SimpleAttributeSet();
		StyleConstants.setBold(highlighted, true);
		boolean insideTag = false;
		int beginText = -1; // will contain beginning of text to be highlighted
		for (int i = 0; i < doc.getLength(); i++) {
			char c = ' '; // Initialisation to keep compiler happy
			try {
				c = doc.getText(i, 1).charAt(0);
			} catch (BadLocationException e) {
			}
			if (insideTag) {
				if (c == '>')
					insideTag = false;
			} else { // not inside a tag
				if (c == '<') {
					// Start of new tag
					if (beginText != -1) { // anything to highlight?
						// highlight it
						doc.setCharacterAttributes(beginText, i - beginText, highlighted, false);
						beginText = -1;
					}
					insideTag = true;
				} else { // normal text character
					if (beginText == -1) {
						// This is the first text character
						beginText = i;
					}
				}
			}
		} // for all characters in document
			// Any text at the very end of the document?
		if (beginText != -1) {
			doc.setCharacterAttributes(beginText, doc.getLength() - beginText, highlighted, false);
		}
	}

	// Call the mary client
	private void processInput() {
		OutputStream os;
		MaryClient.DataType outputType = (MaryClient.DataType) cbOutputType.getSelectedItem();
		if (outputType.name().equals("AUDIO")) {
			try {
				audioPlayer = new marytts.util.data.audio.AudioPlayer();
				processor.streamAudio(inputText.getText(), ((MaryClient.DataType) cbInputType.getSelectedItem()).name(),
						((MaryClient.Voice) cbDefaultVoice.getSelectedItem()).getLocale().toString(), streamMp3 ? "MP3" : "AU",
						((MaryClient.Voice) cbDefaultVoice.getSelectedItem()).name(), "", getAudioEffectsString(), audioPlayer,
						new MaryHttpClient.AudioPlayerListener() {
							public void playerFinished() {
								resetPlayButton();
							}

							public void playerException(Exception e) {
								showErrorMessage(e.getClass().getName(), e.getMessage());
								resetPlayButton();
							}
						});
				bPlay.setText("Stop");
			} catch (Exception e) {
				e.printStackTrace();
				showErrorMessage(e.getClass().getName(), e.getMessage());
				resetPlayButton();
			}

		} else {
			try {
				// Write to a byte array (to be converted to a string later)
				os = new ByteArrayOutputStream();
				MaryClient.Voice voice = (MaryClient.Voice) cbDefaultVoice.getSelectedItem();
				String voiceName = voice != null ? voice.name() : null;
				String locale = voice != null ? voice.getLocale().toString() : null;
				processor.process(inputText.getText(), ((MaryClient.DataType) cbInputType.getSelectedItem()).name(),
						outputType.name(), locale, null, voiceName, "", getAudioEffectsString(), null, os);

				try {
					setOutputText(((ByteArrayOutputStream) os).toString("UTF-8"));
				} catch (UnsupportedEncodingException uee) {
					uee.printStackTrace();
				}
				bEdit.setEnabled(true);
			} catch (Exception e) {
				e.printStackTrace();
				showErrorMessage(e.getClass().getName(), e.getMessage());
			}
		}
	}

	private void editOutput() {
		MaryClient.DataType type = (MaryClient.DataType) cbOutputType.getSelectedItem();
		if (type == null || !type.isTextType() || !type.isInputType())
			return;
		setInputText(outputText.getText());
		setOutputText("");
		// We need to make sure the item handler doesn't try to replace
		// the input with a default example:
		if (cbInputType.getSelectedItem().equals(cbOutputType.getSelectedItem())) {
			// No problem, type won't change anyway
		} else {
			// Signal to the item handler that we don't want replacement
			doReplaceInput = false;
			cbInputType.setSelectedItem(cbOutputType.getSelectedItem());
		}
	}

	private void compareTexts() {
		// Only try to compare if both are MaryXML and non-empty:
		if (!((MaryClient.DataType) cbOutputType.getSelectedItem()).isTextType() || inputText.getText().length() == 0
				|| outputText.getText().length() == 0) {
			return;
		}
		try {
			// First, make both documents plain text:
			makeTextPlain(inputText.getStyledDocument());
			makeTextPlain(outputText.getStyledDocument());

			// Now, highlight text in both documents:
			highlightText(inputText.getStyledDocument());
			highlightText(outputText.getStyledDocument());

			// Define text attributes for added/removed chunks:
			SimpleAttributeSet removed = new SimpleAttributeSet();
			SimpleAttributeSet added = new SimpleAttributeSet();
			StyleConstants.setBold(removed, true);
			StyleConstants.setBold(added, true);
			StyleConstants.setItalic(removed, true);
			StyleConstants.setItalic(added, true);
			StyleConstants.setUnderline(added, true);
			StyleConstants.setForeground(removed, Color.red);
			StyleConstants.setForeground(added, Color.green.darker());
			// Calculate the differences between input and output:
			String input = inputText.getStyledDocument().getText(0, inputText.getStyledDocument().getLength());
			String[] inputWords = MaryUtils.splitIntoSensibleXMLUnits(input);
			int[] inputIndex = new int[inputWords.length + 1];
			int total = 0;
			for (int i = 0; i < inputWords.length; i++) {
				inputIndex[i] = total;
				total += inputWords[i].length();
				// System.err.println("Input Word nr. " + i + ": [" + inputWords[i] + "], indexes " + inputIndex[i] + "-" +
				// (inputIndex[i]+inputWords[i].length()) + "[" + input.substring(inputIndex[i],
				// inputIndex[i]+inputWords[i].length()) + "] / [" + inputText.getStyledDocument().getText(inputIndex[i],
				// inputWords[i].length()) + "]");
			}
			inputIndex[inputWords.length] = total;
			String output = outputText.getStyledDocument().getText(0, outputText.getStyledDocument().getLength());
			String[] outputWords = MaryUtils.splitIntoSensibleXMLUnits(output);
			int[] outputIndex = new int[outputWords.length + 1];
			total = 0;
			for (int i = 0; i < outputWords.length; i++) {
				outputIndex[i] = total;
				total += outputWords[i].length();
				// System.err.println("Output Word nr. " + i + ": [" + outputWords[i] + "], indexes " + outputIndex[i] + "-" +
				// (outputIndex[i]+outputWords[i].length()) + "[" + output.substring(outputIndex[i],
				// outputIndex[i]+outputWords[i].length()) + "]");
			}
			outputIndex[outputWords.length] = total;
			List diffs = new Diff(inputWords, outputWords).diff();
			for (Difference diff : diffs) {
				int delStart = diff.getDeletedStart();
				int delEnd = diff.getDeletedEnd();
				int addStart = diff.getAddedStart();
				int addEnd = diff.getAddedEnd();
				if (delEnd != Difference.NONE) {
					inputText.getStyledDocument().setCharacterAttributes(inputIndex[delStart],
							inputIndex[delEnd + 1] - inputIndex[delStart], removed, false);
					// System.err.println("deleted "+delStart+"-"+(delEnd+1)+": [" + input.substring(inputIndex[delStart],
					// inputIndex[delEnd+1]) + "] / [" + inputText.getStyledDocument().getText(inputIndex[delStart],
					// inputIndex[delEnd+1]-inputIndex[delStart]) + "]");
				}
				if (addEnd != Difference.NONE) {
					outputText.getStyledDocument().setCharacterAttributes(outputIndex[addStart],
							outputIndex[addEnd + 1] - outputIndex[addStart], added, false);
					// System.err.println("added "+addStart+"-"+(addEnd+1)+": [" + output.substring(outputIndex[addStart],
					// outputIndex[addEnd+1]) + "] / [" + outputText.getStyledDocument().getText(outputIndex[addStart],
					// outputIndex[addEnd+1]-outputIndex[addStart]) + "]");
				}
			}
		} catch (Exception ex) {
			ex.printStackTrace();
		}
	}

	protected void setInputText(String text) {
		inputText.setText(text);
		makeTextPlain(inputText.getStyledDocument());
		inputText.setCaretPosition(0);
	}

	protected void setOutputText(String text) {
		outputText.setText(text);
		makeTextPlain(outputText.getStyledDocument());
		outputText.setCaretPosition(0);
	}

	private MaryClient.Voice getPrevVoice() {
		return prevVoice;
	}

	private void setPrevVoice(MaryClient.Voice prevVoice) {
		this.prevVoice = prevVoice;
	}

	public void resetPlayButton() {
		bPlay.setText("Play");
		if (audioPlayer != null) {
			audioPlayer.cancel();
			audioPlayer = null;
		}
	}

	protected void showErrorMessage(String title, String message) {
		JOptionPane.showMessageDialog(this, message + "\n\nIf you think this is a bug in the MARY system,\n"
				+ "please help improve the system by filing a bug report\n" + "on the MARY development page: \n"
				+ "http://mary.opendfki.de/newticket\n", title, JOptionPane.ERROR_MESSAGE);

	}

	public static void main(String[] args) throws Exception {
		mainFrame = new JFrame("Mary GUI Client");
		mainFrame.addWindowListener(new WindowAdapter() {
			public void windowClosing(WindowEvent e) {
				System.exit(0);
			}
		});
		MaryGUIClient m = new MaryGUIClient();
		mainFrame.setContentPane(m);
		mainFrame.pack();
		mainFrame.setVisible(true);

	}

	class MaryGUIFocusTraversalPolicy extends FocusTraversalPolicy {

		public Component getComponentAfter(Container focusCycleRoot, Component aComponent) {
			if (aComponent.equals(cbInputType)) {
				return cbOutputType;
			} else if (aComponent.equals(cbOutputType)) {
				return cbDefaultVoice;
			} else if (aComponent.equals(cbDefaultVoice)) {
				if (cbVoiceExampleText.isVisible()) {
					return cbVoiceExampleText;
				} else {
					return inputText;
				}
			} else if (aComponent.equals(cbVoiceExampleText)) {
				return inputText;
			} else if (aComponent.equals(inputText)) {
				if (audioPanel.isVisible()) {
					return bPlay;
				} else {
					return bProcess;
				}
			} else if (aComponent.equals(bProcess)) {
				if (bEdit.isEnabled()) {
					return bEdit;
				} else {
					if (allowSave && bSaveOutput.isEnabled()) {
						return bSaveOutput;
					} else {
						return cbInputType;
					}
				}
			} else if (aComponent.equals(bPlay)) {
				if (allowSave) {
					return bSaveOutput;
				} else {
					return cbInputType;
				}
			} else if (aComponent.equals(outputText)) {
				if (bEdit.isEnabled()) {
					return bEdit;
				} else {
					return cbInputType;
				}
			} else if (aComponent.equals(bEdit)) {
				return bCompare;
			} else if (aComponent.equals(bCompare)) {
				if (allowSave) {
					return bSaveOutput;
				} else {
					return cbInputType;
				}
			} else if (aComponent.equals(bSaveOutput)) {
				return cbInputType;
			}
			return cbInputType;
		}

		public Component getComponentBefore(Container focusCycleRoot, Component aComponent) {
			if (aComponent.equals(bSaveOutput)) {
				if (!buttonPanel.isVisible()) {

					return bPlay;
				} else {
					if (bCompare.isEnabled()) {

						return bCompare;
					} else {

						return bProcess;
					}
				}
			} else if (aComponent.equals(bCompare)) {
				return bEdit;
			} else if (aComponent.equals(bEdit)) {
				return bProcess;
			} else if (aComponent.equals(outputText)) {
				return bProcess;
			} else if (aComponent.equals(bPlay)) {
				return inputText;
			} else if (aComponent.equals(bProcess)) {
				return inputText;
			} else if (aComponent.equals(inputText)) {
				if (cbVoiceExampleText.isVisible()) {
					return cbVoiceExampleText;
				} else {
					return cbDefaultVoice;
				}
			} else if (aComponent.equals(cbVoiceExampleText)) {
				return cbDefaultVoice;
			} else if (aComponent.equals(cbDefaultVoice)) {
				return cbOutputType;
			} else if (aComponent.equals(cbOutputType)) {
				return cbInputType;
			} else if (aComponent.equals(cbInputType)) {
				if (allowSave && bSaveOutput.isEnabled()) {
					return bSaveOutput;
				} else {
					if (buttonPanel.isVisible()) {
						return bProcess;
					} else {
						return bPlay;
					}
				}
			}
			return cbInputType;
		}

		public Component getDefaultComponent(Container focusCycleRoot) {
			return cbInputType;
		}

		public Component getLastComponent(Container focusCycleRoot) {
			return bSaveOutput;
		}

		public Component getFirstComponent(Container focusCycleRoot) {
			return cbInputType;
		}
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy