import javax.swing.*; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.table.*; import java.awt.*; import java.awt.event.*; import java.awt.image.BufferedImage; import java.beans.*; import java.io.*; import java.net.*; import java.util.*; /** * */ /** * @author Matt Chun-Lum * */ public class Whiteboard extends JFrame { private static final Random generator = new Random(); private static final int NORMAL_MODE = 0; private static final int SERVER_MODE = 1; private static final int CLIENT_MODE = 2; private static int nextID = 0; public static int getNextIDNumber() { return nextID++; } private Box controlGroup; private JLabel addLabel; private JLabel modeLabel; private JTextField textField; private JTable table; private WhiteboardTableModel tableModel; private JComboBox fontSelector; private JButton rectAddButton, ovalAddButton, lineAddButton, textAddButton, moveFrontButton, moveBackButton, removeButton, serverModeButton, clientModeButton; private JMenuBar menuBar; private JFileChooser fileChooser; private ArrayList<JComponent> disableGroup; private HashMap<String, Integer> fontMap; private boolean justUpdatedField; private Canvas canvas; // Networking stuff private ClientHandler clientHandler; private ServerAccepter serverAccepter; private int currentMode; private ArrayList<ObjectOutputStream> outputs = new ArrayList<ObjectOutputStream>(); public Whiteboard() { setLayout(new BorderLayout()); controlGroup = Box.createVerticalBox(); disableGroup = new ArrayList<JComponent>(); currentMode = NORMAL_MODE; setUpFileMenu(); setUpServerClientGroup(); setUpAddButtonGroup(); setUpFontGroup(); setUpManipulationGroup(); setUpTable(); alignControls(); add(controlGroup, BorderLayout.WEST); setUpCanvas(); pack(); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setVisible(true); } /** * Enables the canvas to set the values for the text input * field and the font combo box * @param text * @param font */ public void updateFontGroup(String text, String font) { int index = fontMap.get(font); fontSelector.setSelectedIndex(index); textField.setText(text); justUpdatedField = true; } /** * Adds the shape to the table * @param shape */ public void addToTable(DShape shape) { tableModel.addModel(shape.getModel()); updateTableSelection(shape); } /** * moves the specified shape to the last position in the table * @param shape */ public void didMoveToBack(DShape shape) { tableModel.moveModelToBack(shape.getModel()); updateTableSelection(shape); } /** * moves the specified shape to the first position in the table * @param shape */ public void didMoveToFront(DShape shape) { tableModel.moveModelToFront(shape.getModel()); updateTableSelection(shape); } /** * removes the shape from the table * @param shape */ public void didRemove(DShape shape) { tableModel.removeModel(shape.getModel()); updateTableSelection(null); } /** * Clears the table of all models */ public void clearTable() { updateTableSelection(null); tableModel.clear(); } /** * Selects an appropriate row in the table */ public void updateTableSelection(DShape selected) { table.clearSelection(); if(selected != null) { int index = tableModel.getRowForModel(selected.getModel()); table.setRowSelectionInterval(index, index); } } /** * Adds the ouptut stream to the list of outputs ("clients") * @param out */ public synchronized void addOutput(ObjectOutputStream out) { outputs.add(out); } /** * Starts the whiteboard instance as a server */ public void doServer() { String result = JOptionPane.showInputDialog("Run server on port", "39587"); if(result != null) { disableControls(SERVER_MODE); modeLabel.setText(getModeString(SERVER_MODE)); currentMode = SERVER_MODE; serverAccepter = new ServerAccepter(Integer.parseInt(result.trim())); serverAccepter.start(); } } /** * Starts the whiteboard instance as a client */ public void doClient() { String result = JOptionPane.showInputDialog("Connect to host:port", "127.0.0.1:39587"); if(result != null) { String[] parts = result.split(":"); disableControls(CLIENT_MODE); modeLabel.setText(getModeString(CLIENT_MODE)); currentMode = CLIENT_MODE; clientHandler = new ClientHandler(parts[0].trim(), Integer.parseInt(parts[1].trim())); clientHandler.start(); } } /** * Sends the command for the given model to all clients * @param command * @param model */ public void doSend(int command, DShapeModel model) { Message message = new Message(); message.setCommand(command); message.setModel(model); sendRemote(message); } public String getXMLStringForMessage(Message message) { OutputStream memStream = new ByteArrayOutputStream(); XMLEncoder encoder = new XMLEncoder(memStream); encoder.writeObject(message); encoder.close(); return memStream.toString(); } /** * Passes the message on to all clients * @param message */ public synchronized void sendRemote(Message message) { String xmlString = getXMLStringForMessage(message); Iterator<ObjectOutputStream> it = outputs.iterator(); while(it.hasNext()) { ObjectOutputStream out = it.next(); try { out.writeObject(xmlString); out.flush(); } catch (Exception ex) { ex.printStackTrace(); it.remove(); } } } /** * Reads a message and executes the contained command * @param message */ public void processMessage(final Message message) { SwingUtilities.invokeLater(new Runnable() { public void run() { DShape shape = canvas.getShapeWithID(message.getModel().getID()); switch(message.getCommand()) { case Message.ADD: if(shape == null) canvas.addShape(message.getModel()); break; case Message.REMOVE: if(shape != null) canvas.markForRemoval(shape); break; case Message.BACK: if(shape != null) canvas.moveToBack(shape); break; case Message.FRONT: if(shape != null) canvas.moveToFront(shape); break; case Message.CHANGE: if(shape != null) shape.getModel().mimic(message.getModel()); updateTableSelection(shape); break; default: break; } } }); } /** * @return true if the current instance is not a client */ public boolean isNotClient() { return currentMode != CLIENT_MODE; } /** * * @return true if the current instance is a server */ public boolean isServer() { return currentMode == SERVER_MODE; } /** * @param args */ public static void main(String[] args) { Whiteboard whiteboard = new Whiteboard(); // for easy testing Whiteboard whiteboard2 = new Whiteboard(); } // ----------------------- Private ---------------------- // /** * adds a shape to the canvas */ private void addShape(DShapeModel model) { if(model instanceof DLineModel) ((DLineModel) model).modifyWithPoints(new Point(Canvas.INITIAL_SHAPE_POS, Canvas.INITIAL_SHAPE_POS), new Point(Canvas.INITIAL_SHAPE_POS + Canvas.INITIAL_SHAPE_SIZE, Canvas.INITIAL_SHAPE_POS + Canvas.INITIAL_SHAPE_SIZE)); else model.setBounds(Canvas.INITIAL_SHAPE_POS, Canvas.INITIAL_SHAPE_POS, Canvas.INITIAL_SHAPE_SIZE, Canvas.INITIAL_SHAPE_SIZE); canvas.addShape(model); } /** * Handles a change in the text input field * @param e */ private void handleTextChange(DocumentEvent e) { if(canvas.hasSelected() && canvas.getSelected() instanceof DText) canvas.setTextForSelected(textField.getText()); } /** * Saves the state of the canvas using an XML encoder */ private void saveCanvas() { int retVal = fileChooser.showSaveDialog(this); if(retVal == JFileChooser.APPROVE_OPTION) canvas.saveCanvas(fileChooser.getSelectedFile()); } /** * Opens a saved canvas state using a XML decoder */ private void openCanvas() { int retVal = fileChooser.showOpenDialog(this); if(retVal == JFileChooser.APPROVE_OPTION) { canvas.openCanvas(fileChooser.getSelectedFile()); for(DShape shape : canvas.getShapes()) doSend(Message.ADD, shape.getModel()); } } /** * Saves a canvas state to a PNG file */ private void saveImage() { int retVal = fileChooser.showSaveDialog(this); if(retVal == JFileChooser.APPROVE_OPTION) canvas.saveImage(fileChooser.getSelectedFile()); } /** * Returns the string associated with the passed mode * @param mode * @return */ private String getModeString(int mode) { switch(mode) { case NORMAL_MODE: return "Normal Mode"; case SERVER_MODE: return "Server Mode"; case CLIENT_MODE: return "Client Mode"; default: return "Error"; } } private void disableControls(int mode) { clientModeButton.setEnabled(false); serverModeButton.setEnabled(false); if(mode == CLIENT_MODE) for(JComponent comp : disableGroup) comp.setEnabled(false); } // --------- Methods for generating GUI elements ---------- // // creates the file menu for opening/saving the current state of the canvas private void setUpFileMenu() { menuBar = new JMenuBar(); JMenu fileMenu = new JMenu("File"); JMenuItem saveItem = new JMenuItem("Save Canvas"); saveItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { saveCanvas(); } }); JMenuItem openItem = new JMenuItem("Open Canvas"); openItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { openCanvas(); } }); JMenuItem saveImageItem = new JMenuItem("Save Image"); saveImageItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { saveImage(); } }); fileMenu.add(saveItem); fileMenu.add(openItem); fileMenu.addSeparator(); fileMenu.add(saveImageItem); menuBar.add(fileMenu); setJMenuBar(menuBar); fileChooser = new JFileChooser(); //fileChooser.setCurrentDirectory(new File(".")); disableGroup.add(openItem); } // sets up the buttons and label for the server-client options private void setUpServerClientGroup() { Box panel = Box.createHorizontalBox(); modeLabel = new JLabel(getModeString(NORMAL_MODE)); serverModeButton = new JButton("Set Server Mode"); serverModeButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { doServer(); } }); clientModeButton = new JButton("Set Client Mode"); clientModeButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { doClient(); } }); panel.add(serverModeButton); panel.add(clientModeButton); panel.add(Box.createHorizontalStrut(40)); panel.add(modeLabel); controlGroup.add(panel); } // creates the add label and the buttons to add various elements to the canvas // also sets up the set color button private void setUpAddButtonGroup() { Box panel = Box.createHorizontalBox(); addLabel = new JLabel("Add"); rectAddButton = new JButton("Rect"); rectAddButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { addShape(new DRectModel()); } }); ovalAddButton = new JButton("Oval"); ovalAddButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { addShape(new DOvalModel()); } }); lineAddButton = new JButton("Line"); lineAddButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { addShape(new DLineModel()); } }); textAddButton = new JButton("Text"); textAddButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { addShape(new DTextModel()); } }); panel.add(addLabel); panel.add(rectAddButton); panel.add(ovalAddButton); panel.add(lineAddButton); panel.add(textAddButton); controlGroup.add(panel, BorderLayout.WEST); JButton setColorButton = new JButton("Set Color"); setColorButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { if(canvas.hasSelected()) { Color newColor = JColorChooser.showDialog(Whiteboard.this, "Select Shape Color", canvas.getSelected().getColor()); if(newColor != null && !newColor.equals(canvas.getSelected().getColor())) { canvas.setSelectedColor(newColor); } } } }); controlGroup.add(setColorButton, BorderLayout.WEST); disableGroup.add(rectAddButton); disableGroup.add(ovalAddButton); disableGroup.add(lineAddButton); disableGroup.add(textAddButton); disableGroup.add(setColorButton); } // creates the text area for setting the text on a text element // also creates the font selection private void setUpFontGroup() { Box panel = Box.createHorizontalBox(); textField = new JTextField(""); textField.setMaximumSize(new Dimension(200, 20)); textField.setPreferredSize(new Dimension(200, 20)); textField.getDocument().addDocumentListener(new DocumentListener() { public void changedUpdate(DocumentEvent e) { } public void insertUpdate(DocumentEvent e) { handleTextChange(e); } public void removeUpdate(DocumentEvent e) { handleTextChange(e); } }); GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); String fonts[] = ge.getAvailableFontFamilyNames(); fontMap = new HashMap<String, Integer>(); for(int i = 0; i < fonts.length; i++) { fontMap.put(fonts[i], i); } fontSelector = new JComboBox(fonts); fontSelector.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { if(canvas.hasSelected() && canvas.getSelected() instanceof DText) canvas.setFontForSelected((String) fontSelector.getSelectedItem()); } }); panel.add(textField); panel.add(fontSelector); controlGroup.add(panel, BorderLayout.WEST); } //creates the move-to button set and the remove button private void setUpManipulationGroup() { Box panel = Box.createHorizontalBox(); moveFrontButton = new JButton("Move To Front"); moveFrontButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { if(canvas.hasSelected()) canvas.moveSelectedToFront(); } }); moveBackButton = new JButton("Move To Back"); moveBackButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { if(canvas.hasSelected()) canvas.moveSelectedToBack(); } }); removeButton = new JButton("Remove Shape"); removeButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { if(canvas.hasSelected()) canvas.markSelectedShapeForRemoval(); } }); panel.add(moveFrontButton); panel.add(moveBackButton); panel.add(removeButton); controlGroup.add(panel, BorderLayout.WEST); disableGroup.add(textField); disableGroup.add(fontSelector); disableGroup.add(moveFrontButton); disableGroup.add(moveBackButton); disableGroup.add(removeButton); } // sets up the table private void setUpTable( ) { tableModel = new WhiteboardTableModel(); table = new JTable(tableModel); table.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS); JScrollPane scrollpane = new JScrollPane(table); scrollpane.setPreferredSize(new Dimension(300, 200)); controlGroup.add(scrollpane, BorderLayout.WEST); } // sets up the canvas private void setUpCanvas() { canvas = new Canvas(this); add(canvas, BorderLayout.CENTER); } // aligns the controls private void alignControls() { for(Component comp : controlGroup.getComponents()) ((JComponent) comp).setAlignmentX(Box.LEFT_ALIGNMENT); } // ------------------ Client Server Handling --------------- // // Adapted from Handout 32 // Thread that handles incoming messages from a server instance private class ClientHandler extends Thread { private String name; private int port; public ClientHandler(String name, int port) { this.name = name; this.port = port; } public void run() { try { Socket toServer = new Socket(name, port); ObjectInputStream in = new ObjectInputStream(toServer.getInputStream()); while(true) { String xmlString = (String) in.readObject(); XMLDecoder decoder = new XMLDecoder(new ByteArrayInputStream(xmlString.getBytes())); Message message = (Message) decoder.readObject(); processMessage(message); } } catch (Exception ex) { ex.printStackTrace(); } } } // Thread that handles incoming connections private class ServerAccepter extends Thread { private int port; public ServerAccepter(int port) { this.port = port; } public void run() { try { ServerSocket serverSocket = new ServerSocket(port); while(true) { Socket toClient = null; toClient = serverSocket.accept(); final ObjectOutputStream out = new ObjectOutputStream(toClient.getOutputStream()); if(!outputs.contains(out)) { Thread worker = new Thread(new Runnable() { public void run() { for(DShape shape : canvas.getShapes()) try { out.writeObject(getXMLStringForMessage(new Message(Message.ADD, shape.getModel()))); out.flush(); } catch (Exception ex) { ex.printStackTrace(); } } }); worker.start(); } addOutput(out); // Send the models already in the canvas to the client } } catch (Exception ex) { ex.printStackTrace(); } } } // Struct object for network commmunication public static class Message { public static final int ADD = 0; public static final int REMOVE = 1; public static final int FRONT = 2; public static final int BACK = 3; public static final int CHANGE = 4; public int command; public DShapeModel model; public Message() { command = -1; model = null; } public Message(int command, DShapeModel model) { this.command = command; this.model = model; } public int getCommand() { return command; } public void setCommand(int cmd) { command = cmd; } public DShapeModel getModel() { return model; } public void setModel(DShapeModel model) { this.model = model; } } }