DasherJava / src / dasherJava / gui / MainFrame.java
MainFrame.java
Raw
package dasherJava.gui;

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.IllegalComponentStateException;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSpinner;
import javax.swing.JSplitPane;
import javax.swing.JTextArea;
import javax.swing.ScrollPaneConstants;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;
import javax.swing.event.ChangeListener;
import javax.swing.plaf.basic.BasicComboPopup;
import javax.swing.plaf.basic.ComboPopup;
import javax.swing.plaf.metal.MetalComboBoxUI;
import javax.swing.text.BadLocationException;

import dasherJava.DasherJava;
import dasherJava.DasherJava.ViewPanelBounds;
import dasherJava.core.input.InputProvider;
import dasherJava.core.output.PauseDasherTarget;
import dasherJava.core.output.TextCharOutput;
import dasherJava.core.settings.Settings;
import dasherJava.core.startStop.StartStopHandler;
import dasherJava.core.world.SquareWorldView;
import dasherJava.core.world.WorldUpdateThread;

public class MainFrame extends DefaultLocationJFrame {
	
	private final TextCharOutput internalTextCharOutput;
	private final SettingsDialog settingsDialog;
	private final JScrollPane textAreaScrollPane;
	private final JTextArea textArea = new JTextArea(3, 0); //show 3 rows by default
	private final JPanel statusBar = new JPanel(null);
	private final JSpinner movementSpeedSpinner = SettingsDialog.createDoubleSpinner(0.0, 10.0, 0.001);
	private final JComboBox<String> alphabetComboBox = new JComboBox<>();
	
	private final ChangeListener movementSpeedSpinnerListener = e -> {
		float newValue = SettingsDialog.readDoubleSpinner((JSpinner) e.getSource());
		DasherJava.getSettings().setMovementSpeed(newValue);
	};
	private final ActionListener alphabetComboBoxActionListener = e -> {
		String name = (String) alphabetComboBox.getSelectedItem();
		if (name==null) return; //no selection
		DasherJava.changeAlphabet(name);
	};
	
	private volatile boolean isCurrentlyShowing = false; //field to be read by other threads
	private WorldPanel worldPanel;
	private WorldUpdateThread worldUpdateThread;
	
	public MainFrame() {
		super("DasherJava "+DasherJava.VERSION_STRING);
		settingsDialog=new SettingsDialog(this);
		setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
		addWindowListener(new WindowAdapter() {
			@Override
			public void windowClosing(WindowEvent e) {
				DasherJava.doExit(true);
			}
			@Override
			public void windowIconified(WindowEvent e) {
				isCurrentlyShowing=false;
			}
			@Override
			public void windowDeiconified(WindowEvent e) {
				isCurrentlyShowing=true;
			}
		});
		addComponentListener(new ComponentAdapter() {
			@Override
			public void componentHidden(ComponentEvent e) {
				isCurrentlyShowing=false;
			}
			@Override
			public void componentShown(ComponentEvent e) {
				isCurrentlyShowing=true;
			}
		});
		setLayout(new BorderLayout());
		textArea.setLineWrap(true);
		textArea.setWrapStyleWord(true);
		textAreaScrollPane=new JScrollPane(textArea, ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS,
				ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
		internalTextCharOutput=new TextCharOutput() {
			@Override
			public void outputChar(int unicode) {
				SwingUtilities.invokeLater(() -> {
					textArea.requestFocusInWindow();
					int caretPosition = textArea.getCaretPosition();
					String toInsert = Character.toString(unicode);
					textArea.setText(getText(0, caretPosition)+toInsert
							+getText(caretPosition, textArea.getText().length()));
					textArea.setCaretPosition(caretPosition+toInsert.length());
				});
			}
			@Override
			public void deleteLastChar() {
				SwingUtilities.invokeLater(() -> {
					textArea.requestFocusInWindow();
					int caretPosition = textArea.getCaretPosition();
					if (caretPosition<=0) return; //caret at start of text
					textArea.setText(getText(0, caretPosition-1)+getText(caretPosition, textArea.getText().length()));
					textArea.setCaretPosition(caretPosition-1);
				});
			}
			@Override
			public void deleteText(TextRange range) {
				SwingUtilities.invokeLater(() -> {
					textArea.requestFocusInWindow();
					switch (range) {
						case TEXT_RANGE_ALL:
							textArea.setText("");
							break;
						case TEXT_RANGE_CHAR:
							int caretPosition = textArea.getCaretPosition();
							if (caretPosition>=textArea.getText().length()) return; //caret at end of text
							textArea.setText(getText(0, caretPosition)
									+getText(caretPosition+1, textArea.getText().length()));
							textArea.setCaretPosition(caretPosition);
							break;
						case TEXT_RANGE_WORD:
						case TEXT_RANGE_SENTENCE:
						case TEXT_RANGE_LINE:
						case TEXT_RANGE_PARAGRAPH:
							break; //not yet implemented
					}
				});
			}
			@Override
			public void moveTextCaret(TextTarget target) {
				textArea.requestFocusInWindow();
				SwingUtilities.invokeLater(() -> {
					switch (target) {
						case TEXT_TARGET_START:
							textArea.setCaretPosition(0);
							break;
						case TEXT_TARGET_END:
							textArea.setCaretPosition(textArea.getText().length());
							break;
						case TEXT_TARGET_PREVIOUS_CHAR:
							textArea.setCaretPosition(Math.max(textArea.getCaretPosition()-1, 0));
							break;
						case TEXT_TARGET_NEXT_CHAR:
							textArea.setCaretPosition(Math.min(textArea.getCaretPosition()+1,
									textArea.getText().length()));
							break;
						case TEXT_TARGET_PREVIOUS_WORD:
						case TEXT_TARGET_NEXT_WORD:
						case TEXT_TARGET_PREVIOUS_SENTENCE:
						case TEXT_TARGET_NEXT_SENTENCE:
						case TEXT_TARGET_PREVIOUS_LINE:
						case TEXT_TARGET_NEXT_LINE:
						case TEXT_TARGET_PREVIOUS_PARAGRAPH:
						case TEXT_TARGET_NEXT_PARAGRAPH:
							break; //not yet implemented
					}
				});
			}
		};
		statusBar.setLayout(new BoxLayout(statusBar, BoxLayout.LINE_AXIS));
		statusBar.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
		JLabel speedLabel = new JLabel("Speed:");
		speedLabel.setAlignmentY(0.55f); //move label slightly upwards to improve appearance/alignment
		statusBar.add(speedLabel);
		statusBar.add(Box.createRigidArea(new Dimension(1, 0)));
		movementSpeedSpinner.setMaximumSize(movementSpeedSpinner.getPreferredSize());
		statusBar.add(movementSpeedSpinner);
		statusBar.add(Box.createRigidArea(new Dimension(5, 0)));
		JLabel alphabetLabel = new JLabel("Alphabet:");
		alphabetLabel.setAlignmentY(0.55f); //move label slightly upwards to improve appearance/alignment
		statusBar.add(alphabetLabel);
		statusBar.add(Box.createRigidArea(new Dimension(1, 0)));
		alphabetComboBox.setMinimumSize(new Dimension(0, 0));
		alphabetComboBox.addActionListener(alphabetComboBoxActionListener);
		//System.out.println(UIManager.getUI(alphabetComboBox)); //to find out the class name of the combo box UI
		alphabetComboBox.setUI(new MetalComboBoxUI() { //adapted from https://stackoverflow.com/a/50380449
			@Override
			protected ComboPopup createPopup() {
				return new BasicComboPopup(comboBox) {
					@Override
					protected Rectangle computePopupBounds(int px, int py, int pw, int ph) {
						return super.computePopupBounds(px, py, Math.max(comboBox.getPreferredSize().width,
								comboBox.getWidth()), ph);
					}
				};
			}
		});
		statusBar.add(alphabetComboBox);
		statusBar.add(Box.createRigidArea(new Dimension(5, 0)));
		JButton settingsButton = new JButton("Settings...");
		settingsButton.addActionListener(e -> DasherJava.showSettingsDialog());
		statusBar.add(settingsButton);
	}
	
	public boolean isCurrentlyShowing() {
		return isCurrentlyShowing;
	}
	
	public void setContent(SquareWorldView worldView, SwingWorldGraphics worldGraphics,
	                       StartStopHandler startStopHandler, InputProvider inputProvider) {
		boolean wasStarted = false;
		if (worldUpdateThread!=null) {
			wasStarted=worldUpdateThread.isStarted();
			worldUpdateThread.terminate();
		}
		worldPanel=new WorldPanel(worldView, worldGraphics);
		worldUpdateThread=new WorldUpdateThread(worldView, startStopHandler,
				inputProvider!=null ? inputProvider : worldPanel, worldPanel, wasStarted);
		worldPanel.setWorldUpdateThread(worldUpdateThread);
		worldUpdateThread.start();
		Settings settings = DasherJava.getSettings();
		updateStatusBar(settings);
		getContentPane().removeAll();
		if (settings.getTextOutputTarget().equals("internal")) {
			JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, true, textAreaScrollPane, worldPanel);
			splitPane.setOneTouchExpandable(true);
			add(splitPane, BorderLayout.CENTER);
		} else add(worldPanel, BorderLayout.CENTER);
		if (settings.getShowStatusBar()) add(statusBar, BorderLayout.PAGE_END);
		setAlwaysOnTop(settings.getWindowAlwaysOnTopResult());
		setFocusableWindowState(!settings.getWindowUnfocusableResult());
		setSize(settings.getWindowWidthResult(), settings.getWindowHeightResult());
		int windowX = settings.getWindowXResult();
		int windowY = settings.getWindowYResult();
		if (windowX==Integer.MIN_VALUE && windowY==Integer.MIN_VALUE) {
			if (!isVisible()) //may not call setLocationByPlatform(true) while the window is visible
				setLocationByPlatform(true);
		} else setLocation(windowX, windowY);
		if (isVisible()) {
			revalidate(); //must revalidate after adding/removing components
			repaint(); //must repaint after revalidating
		}
		setVisible(true); //brings the window to the front if it was already visible
	}
	
	public ViewPanelBounds getViewPanelBoundsOnScreen() {
		if (worldPanel==null) return null;
		try {
			Point p = worldPanel.getLocationOnScreen();
			return new ViewPanelBounds(p.x, p.y, worldPanel.getWidth(), worldPanel.getHeight());
		} catch (IllegalComponentStateException ex) { //not showing on screen
			return null;
		}
	}
	
	public TextCharOutput getInternalTextCharOutput() {
		return internalTextCharOutput;
	}
	
	public PauseDasherTarget getPauseDasherTarget() {
		return worldUpdateThread;
	}
	
	public SettingsDialog getSettingsDialog() {
		return settingsDialog;
	}
	
	public boolean doesTextAreaContainText() {
		return !textArea.getText().isEmpty();
	}
	
	public void writeWindowParameters(Settings settings) {
		Rectangle bounds = getBounds();
		settings.setWindowX(bounds.x);
		settings.setWindowY(bounds.y);
		settings.setWindowWidth(bounds.width);
		settings.setWindowHeight(bounds.height);
	}
	
	private String getText(int start, int end) {
		try {
			return textArea.getText(start, end-start);
		} catch (BadLocationException ex) {
			DasherJava.showErrorMessage("MainFrame: getText()", ex);
			return "";
		}
	}
	
	private void updateStatusBar(Settings settings) {
		//Need to remove the listener before setting the value because ChangeListeners apparently fire before
		//the value actually changed, so the listener would always reset the setting to its previous value.
		movementSpeedSpinner.removeChangeListener(movementSpeedSpinnerListener);
		movementSpeedSpinner.setValue(settings.getMovementSpeed());
		movementSpeedSpinner.addChangeListener(movementSpeedSpinnerListener);
		populateAlphabetComboBox(settings);
	}
	
	private void populateAlphabetComboBox(Settings settings) {
		alphabetComboBox.removeActionListener(alphabetComboBoxActionListener); //prevent the listener from being called
		                                                                       //while the combo box is being modified
		alphabetComboBox.removeAllItems();
		for (String alphabetName : settings.getAlphabetHistory()) {
			alphabetComboBox.addItem(alphabetName);
		}
		alphabetComboBox.setSelectedItem(settings.getAlphabetName());
		alphabetComboBox.addActionListener(alphabetComboBoxActionListener);
	}
}