import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.Font;
import java.awt.image.BufferedImage;
import java.text.NumberFormat;
import javax.swing.text.NumberFormatter;
import javax.swing.JFormattedTextField;
import javax.swing.border.EmptyBorder;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JMenuBar;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import java.awt.Graphics;
import javax.imageio.ImageIO;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeEvent;
import java.io.File;
import java.io.PrintWriter;
import java.io.IOException;
import java.util.Scanner;
/*
* Author: Ri Xin Yang
* Date: July 9, 2019
* Desc: This class is a JFrame sets up what is on the frame to be displayed in the application.
*/
public class DrawFrame extends JFrame {
// Declaration of required variables for the draw frame.
private static final int LINE_MODE = 0;
private static final int MAX_ROW_COUNT = 13;
private JLabel statusBar;
private DrawPanel drawPanel;
private String[] colorNames = { "Black", "White", "Red", "Green", "Blue", "Cyan", "Pink", "Yellow", "Magenta",
"Orange", "Gray" };
private Color[] colorValues = { Color.BLACK, Color.WHITE, Color.RED, Color.GREEN, Color.BLUE, Color.CYAN,
Color.PINK, Color.YELLOW, Color.MAGENTA, Color.ORANGE, Color.GRAY };
private String[] shapeNames = { "Line", "Rectangle", "Oval" };
private JComboBox colorChooser;
private JComboBox gradientChooser;
private JComboBox shapeChooser;
private JFormattedTextField lineWidthTextField;
private JFormattedTextField dashLengthTextField;
private JButton undoButton;
private JButton redoButton;
private JButton clearButton;
private JPanel interactionPanel;
private JCheckBox fillBox;
private JCheckBox gradientBox;
private JCheckBox dashBox;
private JLabel lineWidthPrompt;
private JMenuBar menuBar;
private JMenu fileMenu;
private JMenuItem aboutItem;
private JMenuItem saveItem;
private JMenuItem prefItem;
private JMenuItem exitItem;
private int shapeColorIndex;
private int shapeMode;
private int lineWidth;
private int dashLength;
private NumberFormatter inputFormatter;
JCheckBox defaultDashBox;
JCheckBox defaultFilledBox;
JCheckBox defaultGradientBox;
JComboBox defaultColorChooser;
JComboBox defaultGradientChooser;
JComboBox defaultShapeChooser;
JFormattedTextField defaultLineWidthTextField;
JFormattedTextField defaultDashLengthTextField;
// A constructor that creates the frame for display.
public DrawFrame() {
// Call parent constructor to initiate window title.
super("JavaPaint");
// Initialize objects used to create panel.
statusBar = new JLabel();
drawPanel = new DrawPanel(statusBar);
// Initialize all the required JObjects for the frame.
interactionPanel = new JPanel();
clearButton = new JButton("Clear");
fillBox = new JCheckBox("Filled");
gradientBox = new JCheckBox("Gradient");
dashBox = new JCheckBox("Dashed");
redoButton = new JButton("Redo");
undoButton = new JButton("Undo");
colorChooser = new JComboBox(colorNames);
gradientChooser = new JComboBox(colorNames);
shapeChooser = new JComboBox(shapeNames);
lineWidthPrompt = new JLabel("Line Width:");
menuBar = new JMenuBar();
fileMenu = new JMenu("Files");
aboutItem = new JMenuItem("About");
saveItem = new JMenuItem("Save as PNG");
prefItem = new JMenuItem("Preferences");
exitItem = new JMenuItem("Exit");
// Create listeners.
ActionListener buttonListener = new ButtonEventListener();
ActionListener menuListener = new MenuEventListener();
PropertyChangeListener textListener = new TextChangeListener();
ItemListener comboBoxListener = new ComboBoxEventListener();
ItemListener checkBoxListener = new CheckBoxEventListener();
// Set up menu.
menuBar.add(fileMenu);
fileMenu.add(aboutItem);
fileMenu.add(saveItem);
fileMenu.add(prefItem);
fileMenu.add(exitItem);
// Set up formatter for text fields
NumberFormat inputFormat = NumberFormat.getInstance();
inputFormatter = new NumberFormatter(inputFormat);
inputFormatter.setValueClass(Integer.class);
inputFormatter.setMinimum(1);
inputFormatter.setMaximum(300);
inputFormatter.setAllowsInvalid(false);
inputFormatter.setCommitsOnValidEdit(true);
// Initiate text field.
lineWidthTextField = new JFormattedTextField(inputFormatter);
lineWidthTextField.setValue(1);
dashLengthTextField = new JFormattedTextField(inputFormatter);
dashLengthTextField.setValue(1);
// Associate buttons with event listeners.
undoButton.addActionListener(buttonListener);
redoButton.addActionListener(buttonListener);
clearButton.addActionListener(buttonListener);
// Associate menu items with event listeners.
aboutItem.addActionListener(menuListener);
saveItem.addActionListener(menuListener);
prefItem.addActionListener(menuListener);
exitItem.addActionListener(menuListener);
// Associate text-fields with property change listeners.
lineWidthTextField.addPropertyChangeListener(textListener);
dashLengthTextField.addPropertyChangeListener(textListener);
// Set JComboBox events and properties
colorChooser.setMaximumRowCount(MAX_ROW_COUNT);
gradientChooser.setMaximumRowCount(MAX_ROW_COUNT);
shapeChooser.setMaximumRowCount(MAX_ROW_COUNT);
colorChooser.addItemListener(comboBoxListener);
gradientChooser.addItemListener(comboBoxListener);
shapeChooser.addItemListener(comboBoxListener);
// Check box events.
fillBox.addItemListener(checkBoxListener);
gradientBox.addItemListener(checkBoxListener);
dashBox.addItemListener(checkBoxListener);
// Setting default enabled properties.
gradientChooser.setEnabled(false);
fillBox.setEnabled(false);
dashLengthTextField.setEnabled(false);
// Set layout and overall set up of interactionPanel.
interactionPanel.setLayout(new GridLayout(12, 1, 10, 15));
interactionPanel.setBorder(new EmptyBorder(10, 10, 10, 10));
interactionPanel.add(undoButton);
interactionPanel.add(redoButton);
interactionPanel.add(clearButton);
interactionPanel.add(colorChooser);
interactionPanel.add(shapeChooser);
interactionPanel.add(fillBox);
interactionPanel.add(gradientBox);
interactionPanel.add(gradientChooser);
interactionPanel.add(lineWidthPrompt);
interactionPanel.add(lineWidthTextField);
interactionPanel.add(dashBox);
interactionPanel.add(dashLengthTextField);
// Append interactions panel to main panel.
add(interactionPanel, BorderLayout.EAST);
add(menuBar, BorderLayout.NORTH);
// Initialize GUI elements to java window to specific regions.
add(statusBar, BorderLayout.SOUTH);
add(drawPanel, BorderLayout.CENTER);
// Optional config loading.
loadDefaultPreferences();
}
// Inner class for button event handling
class ButtonEventListener implements ActionListener {
// The ActionListener interface requires that we override the actionPerformed()
// method.
// This method will be called automatically whenever a button event occurs.
@Override
public void actionPerformed(ActionEvent e) {
// The ActionEvent getSource() method returns a reference to the button widget
// that was clicked.
// This allows us to use one event listener for more than one JButton, if
// desired.
// Undo button, remove the latest action from shapes, add it to the redo stack.
if (e.getSource() == undoButton) {
drawPanel.undo();
}
// Redo button, pop the redo stack, add it to the latest change of shapes if
// possible.
else if (e.getSource() == redoButton) {
drawPanel.redo();
}
// Clear button, clear all shapes and redo stack as required.
else if (e.getSource() == clearButton) {
drawPanel.clear();
}
}
}
// Inner class for menu event handling
class MenuEventListener implements ActionListener {
// The ActionListener interface requires that we override the actionPerformed()
// method.
// This method will be called automatically whenever a button event occurs.
@Override
public void actionPerformed(ActionEvent e) {
// The ActionEvent getSource() method returns a reference to the button widget
// that was clicked.
// This allows us to use one event listener for more than one JButton, if
// desired.
// Call menu to save preference.
if (e.getSource() == prefItem) {
// Get result back from preference dialog.
int result = JOptionPane.showConfirmDialog(null, getPrefPanel(), "Preferences: Default Settings",
JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE, null);
// If users has sent their inputs, use them to write down the config.
if (result == JOptionPane.OK_OPTION) {
writeDefaultPreferences();
}
}
// Show information about the program on called.
else if (e.getSource() == aboutItem) {
// Show about dialog via JOptionPane.
JOptionPane.showMessageDialog(null, getAboutPanel(), "About", JOptionPane.PLAIN_MESSAGE, null);
}
// Call menu to save drawPanel as png.
else if (e.getSource() == saveItem) {
JFileChooser fileChooser = new JFileChooser();
// Create image with correct resolution to be saved.
BufferedImage image = new BufferedImage(drawPanel.getWidth(), drawPanel.getHeight(), BufferedImage.TYPE_INT_RGB);
// Save drawPanel on image.
Graphics g = image.getGraphics();
drawPanel.print(g);
g.dispose();
// Show file chooser to save file.
int result = fileChooser.showSaveDialog(null);
if (result == JFileChooser.APPROVE_OPTION) {
File selectedFile = new File(fileChooser.getSelectedFile()+".png");
// Try to save image given all required parameters.
try {
ImageIO.write(image, "png", selectedFile);
}
// Catches where the selected file cannot be chosen.
catch (IOException exception) {
System.err.println("Java Exception: " + exception);
System.out.println("Sorry, error with saving image to file.");
}
}
}
// Exit program upon called.
else if (e.getSource() == exitItem) {
System.exit(0);
}
}
}
// Inner class for text input change handling
class TextChangeListener implements PropertyChangeListener {
// The ActionListener interface requires that we override the actionPerformed()
// method.
// This method will be called automatically whenever a button event occurs.
@Override
public void propertyChange(PropertyChangeEvent e) {
// The ActionEvent getSource() method returns a reference to the button widget
// that was clicked.
// This allows us to use one event listener for more than one JButton, if
// desired.
// Change lineWidth on detection.
if (e.getSource() == lineWidthTextField) {
lineWidth = (Integer) (lineWidthTextField.getValue());
setLineWidthTextField(lineWidth);
}
// Change dashLength on detection.
else if (e.getSource() == dashLengthTextField) {
dashLength = (Integer) (dashLengthTextField.getValue());
setDashLengthTextField(dashLength);
}
}
}
// Inner class for handling events on lockBox (JCheckBox)
class CheckBoxEventListener implements ItemListener {
// Override the itemStateChanged() method as required by the ActionListener
// Interface
@Override
public void itemStateChanged(ItemEvent e) {
// Determine the isFilled flag with the check box. Change a set of flags accordingly.
if (fillBox.isSelected()) {
setFillBox(true);
} else {
setFillBox(false);
}
// Determine the isGradient flag with the check box. Change a set of flags accordingly.
if (gradientBox.isSelected()) {
setGradientBox(true);
} else {
setGradientBox(false);
}
// Determine the isDashed flag with the check box. Change a set of flags accordingly.
if (dashBox.isSelected()) {
setDashBox(true);
} else {
setDashBox(false);
}
}
}
// Inner class for handling events on comboBox (JComboBox)
class ComboBoxEventListener implements ItemListener {
// We override the itemStateChanged() method as required by the ActionListener
// Interface
@Override
public void itemStateChanged(ItemEvent e) {
// Change shapeColor depending on user selection.
if (e.getSource() == colorChooser) {
shapeColorIndex = colorChooser.getSelectedIndex();
setShapeColorChooser(shapeColorIndex);
}
// Change shapeMode depending on user selection. Note that shapeMode is an
// integer, representing the type of shape.
else if (e.getSource() == shapeChooser) {
shapeMode = shapeChooser.getSelectedIndex();
setShapeModeChooser(shapeMode);
}
// Change selectedColor depending on user selection.
else if (e.getSource() == gradientChooser) {
shapeColorIndex = gradientChooser.getSelectedIndex();
setGradientColorChooser(shapeColorIndex);
}
}
}
// This method creates the about panel to be displayed in a separate dialog in the files menu.
private JPanel getAboutPanel() {
// Create Jlabels
JLabel titleLabel = new JLabel("JavaPaint");
JLabel authorLabel = new JLabel("Made by Ri Xin Yang");
JLabel descLabel = new JLabel("A paint/draw program made in Java");
// Change title label style.
titleLabel.setFont(new Font("Courier", Font.BOLD, 20));
// Set alignments.
titleLabel.setHorizontalAlignment(JLabel.CENTER);
authorLabel.setHorizontalAlignment(JLabel.CENTER);
descLabel.setHorizontalAlignment(JLabel.CENTER);
// Create panel and add elements to it.
JPanel aboutPanel = new JPanel(new GridLayout(3, 1, 5, 3));
aboutPanel.add(titleLabel);
aboutPanel.add(authorLabel);
aboutPanel.add(descLabel);
// Return the result.
return aboutPanel;
}
// This method creates the preference panel to be displayed in a separate dialog in the files menu.
private JPanel getPrefPanel() {
// Set up elements for the panel.
defaultDashBox = new JCheckBox("Dashed");
defaultFilledBox = new JCheckBox("Filled");
defaultGradientBox = new JCheckBox("Gradient");
defaultColorChooser = new JComboBox(colorNames);
defaultGradientChooser = new JComboBox(colorNames);
defaultShapeChooser = new JComboBox(shapeNames);
defaultLineWidthTextField = new JFormattedTextField(inputFormatter);
defaultDashLengthTextField = new JFormattedTextField(inputFormatter);
// Set default values.
defaultLineWidthTextField.setValue(1);
defaultDashLengthTextField.setValue(1);
// Set up prefPanel.
JPanel prefPanel = new JPanel(new GridLayout(13, 1, 5, 10));
prefPanel.add(defaultDashBox);
prefPanel.add(defaultFilledBox);
prefPanel.add(defaultGradientBox);
prefPanel.add(new JLabel("Shape Type"));
prefPanel.add(defaultShapeChooser);
prefPanel.add(new JLabel("Primary Color"));
prefPanel.add(defaultColorChooser);
prefPanel.add(new JLabel("Gradient Color"));
prefPanel.add(defaultGradientChooser);
prefPanel.add(new JLabel("Line Width"));
prefPanel.add(defaultLineWidthTextField);
prefPanel.add(new JLabel("Dash Length"));
prefPanel.add(defaultDashLengthTextField);
// Return the final result.
return prefPanel;
}
// This method takes the current values of the default preferences and write them into a config file.
private void writeDefaultPreferences() {
// Try writing to the config file location.
try {
PrintWriter configOutput = new PrintWriter("../settings/config");
// Write to config and close file after finishing.
configOutput.println("#This is the default preference config. It will be loaded on startup.\n");
configOutput.println("dashed: " + defaultDashBox.isSelected());
configOutput.println("filled: " + defaultFilledBox.isSelected());
configOutput.println("gradient: " + defaultGradientBox.isSelected()+"\n");
configOutput.println("# 0 for line, 1 for rectangles, and 2 for ovals.");
configOutput.println("shape_mode: " + defaultShapeChooser.getSelectedIndex()+"\n");
configOutput.println(
"# choose colors from black, white, red, green, blue, cyan, pink, yellow, magenta, orange, and gray");
configOutput.println("primary_color: " + defaultColorChooser.getSelectedItem());
configOutput.println("gradient_color: " + defaultGradientChooser.getSelectedItem()+"\n");
configOutput.println("line_width: " + defaultLineWidthTextField.getValue());
configOutput.println("dash_length: " + defaultDashLengthTextField.getValue()+"\n");
// Close writing file after finishing.
configOutput.close();
}
// Catch exceptions for IOException in the case that the file is not found. Inform user.
catch (IOException exception) {
System.err.println("Java Exception: " + exception);
System.out.println("Sorry, error with outputting to config file.");
}
}
// Find config file, load it up by changing variables and object properties.
private void loadDefaultPreferences() {
// Try to open config file for reading.
try {
// Set up variables and objects needed for reading.
Scanner configInput = new Scanner(new File("../settings/config"));
String configLine;
String key = "";
String value = "";
int indexFlag;
int unitFlag;
boolean booleanFlag;
Scanner tokens;
// Read all lines of config.
while (configInput.hasNext()) {
// Save each line and read only the first two tokens of each line.
configLine = configInput.nextLine();
tokens = new Scanner(configLine);
// Save key/first token.
if (tokens.hasNext()) {
key = tokens.next();
}
// Save value/second token.
if (tokens.hasNext()) {
value = tokens.next();
}
// Depending on the key, use the value correctly to configure program properties.
switch (key) {
case "#":
// This indicates a comment within the config, ignore immediately.
break;
case "dashed:":
booleanFlag = Boolean.parseBoolean(value);
setDashBox(booleanFlag);
break;
case "filled:":
booleanFlag = Boolean.parseBoolean(value);
setFillBox(booleanFlag);
break;
case "gradient:":
booleanFlag = Boolean.parseBoolean(value);
setGradientBox(booleanFlag);
break;
case "shape_mode:":
try {
indexFlag = Integer.parseInt(value);
}
// If value cannot be parsed into integer.
catch (NumberFormatException exception) {
// Set default to 0 for lines.
indexFlag = 0;
}
// To causes the itemStateChange method to run.
shapeChooser.setSelectedIndex(indexFlag);
break;
case "primary_color:":
indexFlag = findIndexWithValue(colorChooser, value);
// If index not == -1 (not found), continue changing properties.
if (indexFlag != -1) {
// To causes the itemStateChange method to run.
colorChooser.setSelectedIndex(indexFlag);
}
break;
case "gradient_color:":
indexFlag = findIndexWithValue(colorChooser, value);
// If index not == -1 (not found), continue changing properties.
if (indexFlag != -1) {
// To causes the itemStateChange method to run.
gradientChooser.setSelectedIndex(indexFlag);
}
break;
case "line_width:":
try {
unitFlag = Integer.parseInt(value);
}
// If value cannot be parsed into integer.
catch (NumberFormatException exception) {
// Set default to 1.
unitFlag = 1;
}
// This causes the propertyChange method to run.
lineWidthTextField.setValue((Integer) unitFlag);
break;
case "dash_length:":
try {
unitFlag = Integer.parseInt(value);
} catch (NumberFormatException exception) {
// Set default to 1.
unitFlag = 1;
}
// This causes the propertyChange method to run.
dashLengthTextField.setValue((Integer) unitFlag);
break;
}
}
// Close reader after finishing.
configInput.close();
}
// Catches cases where read file is not found. Inform user.
catch (IOException exception) {
System.err.println("Java Exception: " + exception);
System.out.println("Sorry, unable to open the config file for reading. Please create a new one in 'Preferences'.");
}
}
/*
* This method takes a String comboBox and a String value. It loops through the
* comboBox to see if the value is / within the comboBox. If so, return its
* index. If not, return -1.
*/
private int findIndexWithValue(JComboBox comboBox, String value) {
// Create temporary variable.
int index = -1;
// Loop and save index when found.
for (int i = 0; i < comboBox.getItemCount(); i++) {
if (comboBox.getItemAt(i).equalsIgnoreCase(value)) {
index = i;
break;
}
}
// Return final result (index).
return index;
}
/*
* This method correctly toggles fillBox by considering other relevant properties of the
* drawing panel. This method takes a boolean argument and toggles the filled property along with other relevant
* properties.
*/
private void setFillBox(boolean booleanFlag) {
// Toggle fill on.
if (booleanFlag) {
fillBox.setSelected(true);
drawPanel.setIsFilled(true);
lineWidthTextField.setEnabled(false);
dashLengthTextField.setEnabled(false);
dashBox.setEnabled(false);
}
// Toggle fill off.
else {
fillBox.setSelected(false);
drawPanel.setIsFilled(false);
lineWidthTextField.setEnabled(true);
dashLengthTextField.setEnabled(true);
dashBox.setEnabled(true);
}
}
/*
* This method correctly toggles gradientBox by considering other relevant properties of the
* drawing panel. This method takes a boolean argument and toggles the gradient property along with other relevant
* properties.
*/
private void setGradientBox(boolean booleanFlag) {
// Toggle gradient on.
if (booleanFlag) {
gradientBox.setSelected(true);
;
drawPanel.setIsGradient(true);
gradientChooser.setEnabled(true);
}
// Toggle gradient off.
else {
gradientBox.setSelected(false);
;
drawPanel.setIsGradient(false);
gradientChooser.setEnabled(false);
}
}
/*
* This method correctly toggles dashBox by considering other relevant properties of the
* drawing panel. This method takes a boolean argument and toggles the dashed property along with other relevant
* properties.
*/
private void setDashBox(boolean booleanFlag) {
// Toggle dash on.
if (booleanFlag) {
dashBox.setSelected(true);
drawPanel.setIsDashed(true);
dashLengthTextField.setEnabled(true);
}
// Toggle dash off.
else {
dashBox.setSelected(false);
drawPanel.setIsDashed(false);
dashLengthTextField.setEnabled(false);
}
}
/*
* This method correctly sets the shapeMode by considering other relevant properties of the
* drawing panel. This method takes a int argument and sets the shapeMode.
*/
private void setShapeModeChooser(int shapeMode) {
// Disable isFilled checkbox depending on context.
if (shapeMode == LINE_MODE) {
fillBox.setSelected(false);
fillBox.setEnabled(false);
dashBox.setEnabled(true);
lineWidthTextField.setEnabled(true);
} else {
fillBox.setEnabled(true);
}
// Inform drawPanel of shapeMode.
drawPanel.setShapeMode(shapeMode);
}
/*
* This method sets the drawPanel shape color with a given index of the colorValues array.
*/
private void setShapeColorChooser(int shapeColorIndex) {
drawPanel.setShapeColor(colorValues[shapeColorIndex]);
}
/*
* This method sets the drawPanel gradient color with a given index of the colorValues array.
*/
private void setGradientColorChooser(int shapeColorIndex) {
drawPanel.setGradientColor(colorValues[shapeColorIndex]);
}
/*
* This method sets the drawPanel lineWidth with a given int value in pixels.
*/
private void setLineWidthTextField(int lineWidth) {
drawPanel.setLineWidth(lineWidth);
}
/*
* This method sets the drawPanel dashLength with a given int value in pixels.
*/
private void setDashLengthTextField(int lineWidth) {
drawPanel.setDashLength(lineWidth);
}
}