// This example is from the book "Java in a Nutshell, Second Edition". // Written by David Flanagan. Copyright (c) 1997 O'Reilly & Associates. // You may distribute this source code for non-commercial purposes only. // You may study, modify, and use this example for any purpose, as long as // this notice is retained. Note that this example is provided "as is", // WITHOUT WARRANTY of any kind either expressed or implied. import java.awt.*; // ScrollPane, PopupMenu, MenuShortcut, etc. import java.awt.datatransfer.*; // Clipboard, Transferable, DataFlavor, etc. import java.awt.event.*; // New event model. import java.io.*; // Object serialization streams. import java.util.zip.*; // Data compression/decompression streams. import java.util.Vector; // To store the scribble in. import java.util.Properties; // To store printing preferences in. /** * This class places a Scribble component in a ScrollPane container, * puts the ScrollPane in a window, and adds a simple pulldown menu system. * The menu uses menu shortcuts. Events are handled with anonymous classes. */ public class ScribbleFrame extends Frame { /** A very simple main() method for our program. */ public static void main(String[] args) { new ScribbleFrame(); } /** Remember # of open windows so we can quit when last one is closed */ protected static int num_windows = 0; /** Create a Frame, Menu, and ScrollPane for the scribble component */ public ScribbleFrame() { super("ScribbleFrame"); // Create the window. num_windows++; // Count it. ScrollPane pane = new ScrollPane(); // Create a ScrollPane. pane.setSize(300, 300); // Specify its size. this.add(pane, "Center"); // Add it to the frame. Scribble scribble; scribble = new Scribble(this, 500, 500); // Create a bigger scribble area. pane.add(scribble); // Add it to the ScrollPane. MenuBar menubar = new MenuBar(); // Create a menubar. this.setMenuBar(menubar); // Add it to the frame. Menu file = new Menu("File"); // Create a File menu. menubar.add(file); // Add to menubar. // Create three menu items, with menu shortcuts, and add to the menu. MenuItem n, c, q; file.add(n = new MenuItem("New Window", new MenuShortcut(KeyEvent.VK_N))); file.add(c = new MenuItem("Close Window",new MenuShortcut(KeyEvent.VK_W))); file.addSeparator(); // Put a separator in the menu file.add(q = new MenuItem("Quit", new MenuShortcut(KeyEvent.VK_Q))); // Create and register action listener objects for the three menu items. n.addActionListener(new ActionListener() { // Open a new window public void actionPerformed(ActionEvent e) { new ScribbleFrame(); } }); c.addActionListener(new ActionListener() { // Close this window. public void actionPerformed(ActionEvent e) { close(); } }); q.addActionListener(new ActionListener() { // Quit the program. public void actionPerformed(ActionEvent e) { System.exit(0); } }); // Another event listener, this one to handle window close requests. this.addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent e) { close(); } }); // Set the window size and pop it up. this.pack(); this.show(); } /** Close a window. If this is the last open window, just quit. */ void close() { if (--num_windows == 0) System.exit(0); else this.dispose(); } } /** * This class is a custom component that supports scribbling. It also has * a popup menu that allows the scribble color to be set and provides access * to printing, cut-and-paste, and file loading and saving facilities. * Note that it extends Component rather than Canvas, making it "lightweight." */ class Scribble extends Component implements ActionListener { protected short last_x, last_y; // Coordinates of last click. protected Vector lines = new Vector(256,256); // Store the scribbles. protected Color current_color = Color.black; // Current drawing color. protected int width, height; // The preferred size. protected PopupMenu popup; // The popup menu. protected Frame frame; // The frame we are within. /** This constructor requires a Frame and a desired size */ public Scribble(Frame frame, int width, int height) { this.frame = frame; this.width = width; this.height = height; // We handle scribbling with low-level events, so we must specify // which events we are interested in. this.enableEvents(AWTEvent.MOUSE_EVENT_MASK); this.enableEvents(AWTEvent.MOUSE_MOTION_EVENT_MASK); // Create the popup menu using a loop. Note the separation of menu // "action command" string from menu label. Good for internationalization. String[] labels = new String[] { "Clear", "Print", "Save", "Load", "Cut", "Copy", "Paste" }; String[] commands = new String[] { "clear", "print", "save", "load", "cut", "copy", "paste" }; popup = new PopupMenu(); // Create the menu for(int i = 0; i < labels.length; i++) { MenuItem mi = new MenuItem(labels[i]); // Create a menu item. mi.setActionCommand(commands[i]); // Set its action command. mi.addActionListener(this); // And its action listener. popup.add(mi); // Add item to the popup menu. } Menu colors = new Menu("Color"); // Create a submenu. popup.add(colors); // And add it to the popup. String[] colornames = new String[] { "Black", "Red", "Green", "Blue"}; for(int i = 0; i < colornames.length; i++) { MenuItem mi = new MenuItem(colornames[i]); // Create the submenu items mi.setActionCommand(colornames[i]); // in the same way. mi.addActionListener(this); colors.add(mi); } // Finally, register the popup menu with the component it appears over this.add(popup); } /** Specifies big the component would like to be. It always returns the * preferred size passed to the Scribble() constructor */ public Dimension getPreferredSize() { return new Dimension(width, height); } /** This is the ActionListener method invoked by the popup menu items */ public void actionPerformed(ActionEvent event) { // Get the "action command" of the event, and dispatch based on that. // This method calls a lot of the interesting methods in this class. String command = event.getActionCommand(); if (command.equals("clear")) clear(); else if (command.equals("print")) print(); else if (command.equals("save")) save(); else if (command.equals("load")) load(); else if (command.equals("cut")) cut(); else if (command.equals("copy")) copy(); else if (command.equals("paste")) paste(); else if (command.equals("Black")) current_color = Color.black; else if (command.equals("Red")) current_color = Color.red; else if (command.equals("Green")) current_color = Color.green; else if (command.equals("Blue")) current_color = Color.blue; } /** Draw all the saved lines of the scribble, in the appropriate colors */ public void paint(Graphics g) { for(int i = 0; i < lines.size(); i++) { Line l = (Line)lines.elementAt(i); g.setColor(l.color); g.drawLine(l.x1, l.y1, l.x2, l.y2); } } /** * This is the low-level event-handling method called on mouse events * that do not involve mouse motion. Note the use of isPopupTrigger() * to check for the platform-dependent popup menu posting event, and of * the show() method to make the popup visible. If the menu is not posted, * then this method saves the coordinates of a mouse click or invokes * the superclass method. */ public void processMouseEvent(MouseEvent e) { if (e.isPopupTrigger()) // If popup trigger, popup.show(this, e.getX(), e.getY()); // pop up the menu. else if (e.getID() == MouseEvent.MOUSE_PRESSED) { last_x = (short)e.getX(); last_y = (short)e.getY(); // Save position. } else super.processMouseEvent(e); // Pass other event types on. } /** * This method is called for mouse motion events. It adds a line to the * scribble, on screen, and in the saved representation */ public void processMouseMotionEvent(MouseEvent e) { if (e.getID() == MouseEvent.MOUSE_DRAGGED) { Graphics g = getGraphics(); // Object to draw with. g.setColor(current_color); // Set the current color. g.drawLine(last_x, last_y, e.getX(), e.getY()); // Draw this line lines.addElement(new Line(last_x, last_y, // and save it, too. (short) e.getX(), (short)e.getY(), current_color)); last_x = (short) e.getX(); // Remember current mouse coordinates. last_y = (short) e.getY(); } else super.processMouseMotionEvent(e); // Important! } /** Clear the scribble. Invoked by popup menu */ void clear() { lines.removeAllElements(); // Throw out the saved scribble repaint(); // and redraw everything. } /** Print out the scribble. Invoked by popup menu. */ void print() { // Obtain a PrintJob object. This posts a Print dialog. // printprefs (created below) stores user printing preferences. Toolkit toolkit = this.getToolkit(); PrintJob job = toolkit.getPrintJob(frame, "Scribble", printprefs); // If the user clicked Cancel in the print dialog, then do nothing. if (job == null) return; // Get a Graphics object for the first page of output. Graphics page = job.getGraphics(); // Check the size of the scribble component and of the page. Dimension size = this.getSize(); Dimension pagesize = job.getPageDimension(); // Center the output on the page. Otherwise it would be // be scrunched up in the upper-left corner of the page. page.translate((pagesize.width - size.width)/2, (pagesize.height - size.height)/2); // Draw a border around the output area, so it looks neat. page.drawRect(-1, -1, size.width+1, size.height+1); // Set a clipping region so our scribbles don't go outside the border. // On-screen this clipping happens automatically, but not on paper. page.setClip(0, 0, size.width, size.height); // Print this Scribble component. By default this will just call paint(). // This method is named print(), too, but that is just coincidence. this.print(page); // Finish up printing. page.dispose(); // End the page--send it to the printer. job.end(); // End the print job. } /** This Properties object stores the user print dialog settings. */ private static Properties printprefs = new Properties(); /** * The DataFlavor used for our particular type of cut-and-paste data. * This one will transfer data in the form of a serialized Vector object. * Note that in Java 1.1.1, this works intra-application, but not between * applications. Java 1.1.1 inter-application data transfer is limited to * the pre-defined string and text data flavors. */ public static final DataFlavor dataFlavor = new DataFlavor(Vector.class, "ScribbleVectorOfLines"); /** * Copy the current scribble and store it in a SimpleSelection object * (defined below). Then put that object on the clipboard for pasting. */ public void copy() { // Get system clipboard Clipboard c = this.getToolkit().getSystemClipboard(); // Copy and save the scribble in a Transferable object SimpleSelection s = new SimpleSelection(lines.clone(), dataFlavor); // Put that object on the clipboard c.setContents(s, s); } /** Cut is just like a copy, except we erase the scribble afterwards */ public void cut() { copy(); clear(); } /** * Ask for the Transferable contents of the system clipboard, then ask that * object for the scribble data it represents. If either step fails, beep! */ public void paste() { Clipboard c = this.getToolkit().getSystemClipboard(); // Get clipboard. Transferable t = c.getContents(this); // Get its contents. if (t == null) { // If there is nothing to paste, beep. this.getToolkit().beep(); return; } try { // Ask for clipboard contents to be converted to our data flavor. // This will throw an exception if our flavor is not supported. Vector newlines = (Vector) t.getTransferData(dataFlavor); // Add all those pasted lines to our scribble. for(int i = 0; i < newlines.size(); i++) lines.addElement(newlines.elementAt(i)); // And redraw the whole thing repaint(); } catch (UnsupportedFlavorException e) { this.getToolkit().beep(); // If clipboard has some other type of data } catch (Exception e) { this.getToolkit().beep(); // Or if anything else goes wrong... } } /** * This nested class implements the Transferable and ClipboardOwner * interfaces used in data transfer. It is a simple class that remembers a * selected object and makes it available in only one specified flavor. */ static class SimpleSelection implements Transferable, ClipboardOwner { protected Object selection; // The data to be transferred. protected DataFlavor flavor; // The one data flavor supported. public SimpleSelection(Object selection, DataFlavor flavor) { this.selection = selection; // Specify data. this.flavor = flavor; // Specify flavor. } /** Return the list of supported flavors. Just one in this case */ public DataFlavor[] getTransferDataFlavors() { return new DataFlavor[] { flavor }; } /** Check whether we support a specified flavor */ public boolean isDataFlavorSupported(DataFlavor f) { return f.equals(flavor); } /** If the flavor is right, transfer the data (i.e. return it) */ public Object getTransferData(DataFlavor f) throws UnsupportedFlavorException { if (f.equals(flavor)) return selection; else throw new UnsupportedFlavorException(f); } /** This is the ClipboardOwner method. Called when the data is no * longer on the clipboard. In this case, we don't need to do much. */ public void lostOwnership(Clipboard c, Transferable t) { selection = null; } } /** * Prompt the user for a filename, and save the scribble in that file. * Serialize the vector of lines with an ObjectOutputStream. * Compress the serialized objects with a GZIPOutputStream. * Write the compressed, serialized data to a file with a FileOutputStream. * Don't forget to flush and close the stream. */ public void save() { // Create a file dialog to query the user for a filename. FileDialog f = new FileDialog(frame, "Save Scribble", FileDialog.SAVE); f.show(); // Display the dialog and block. String filename = f.getFile(); // Get the user's response if (filename != null) { // If user didn't click "Cancel". try { // Create the necessary output streams to save the scribble. FileOutputStream fos = new FileOutputStream(filename); // Save to file GZIPOutputStream gzos = new GZIPOutputStream(fos); // Compressed ObjectOutputStream out = new ObjectOutputStream(gzos); // Save objects out.writeObject(lines); // Write the entire Vector of scribbles out.flush(); // Always flush the output. out.close(); // And close the stream. } // Print out exceptions. We should really display them in a dialog... catch (IOException e) { System.out.println(e); } } } /** * Prompt for a filename, and load a scribble from that file. * Read compressed, serialized data with a FileInputStream. * Uncompress that data with a GZIPInputStream. * Deserialize the vector of lines with a ObjectInputStream. * Replace current data with new data, and redraw everything. */ public void load() { // Create a file dialog to query the user for a filename. FileDialog f = new FileDialog(frame, "Load Scribble", FileDialog.LOAD); f.show(); // Display the dialog and block. String filename = f.getFile(); // Get the user's response if (filename != null) { // If user didn't click "Cancel". try { // Create necessary input streams FileInputStream fis = new FileInputStream(filename); // Read from file GZIPInputStream gzis = new GZIPInputStream(fis); // Uncompress ObjectInputStream in = new ObjectInputStream(gzis); // Read objects // Read in an object. It should be a vector of scribbles Vector newlines = (Vector)in.readObject(); in.close(); // Close the stream. lines = newlines; // Set the Vector of lines. repaint(); // And redisplay the scribble. } // Print out exceptions. We should really display them in a dialog... catch (Exception e) { System.out.println(e); } } } /** A class to store the coordinates and color of one scribbled line. * The complete scribble is stored as a Vector of these objects */ static class Line implements Serializable { public short x1, y1, x2, y2; public Color color; public Line(short x1, short y1, short x2, short y2, Color c) { this.x1 = x1; this.y1 = y1; this.x2 = x2; this.y2 = y2; this.color = c; } } }