| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648 |
- // Copyright 2007 Google Inc. All Rights Reserved.
- //
- // Licensed under the Apache License, Version 2.0 (the "License"); You may not
- // use this file except in compliance with the License. You may obtain a copy of
- // the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by
- // applicable law or agreed to in writing, software distributed under the
- // License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
- // OF ANY KIND, either express or implied. See the License for the specific
- // language governing permissions and limitations under the License.
- package com.google.scrollview.ui;
- import com.google.scrollview.ScrollView;
- import com.google.scrollview.events.SVEvent;
- import com.google.scrollview.events.SVEventHandler;
- import com.google.scrollview.events.SVEventType;
- import com.google.scrollview.ui.SVMenuBar;
- import com.google.scrollview.ui.SVPopupMenu;
- import org.piccolo2d.PCamera;
- import org.piccolo2d.PCanvas;
- import org.piccolo2d.PLayer;
- import org.piccolo2d.extras.swing.PScrollPane;
- import org.piccolo2d.nodes.PImage;
- import org.piccolo2d.nodes.PPath;
- import org.piccolo2d.nodes.PText;
- import org.piccolo2d.util.PPaintContext;
- import java.awt.BasicStroke;
- import java.awt.BorderLayout;
- import java.awt.Color;
- import java.awt.Font;
- import java.awt.GraphicsEnvironment;
- import java.awt.Rectangle;
- import java.awt.TextArea;
- import java.awt.geom.IllegalPathStateException;
- import java.util.regex.Matcher;
- import java.util.regex.Pattern;
- import javax.swing.JFrame;
- import javax.swing.JOptionPane;
- import javax.swing.SwingUtilities;
- import javax.swing.WindowConstants;
- /**
- * The SVWindow is the top-level ui class. It should get instantiated whenever
- * the user intends to create a new window. It contains helper functions to draw
- * on the canvas, add new menu items, show modal dialogs etc.
- *
- * @author wanke@google.com
- */
- public class SVWindow extends JFrame {
- /**
- * Constants defining the maximum initial size of the window.
- */
- private static final int MAX_WINDOW_X = 1000;
- private static final int MAX_WINDOW_Y = 800;
- /* Constant defining the (approx) height of the default message box*/
- private static final int DEF_MESSAGEBOX_HEIGHT = 200;
- /** Constant defining the "speed" at which to zoom in and out. */
- public static final double SCALING_FACTOR = 2;
- /** The top level layer we add our PNodes to (root node). */
- PLayer layer;
- /** The current color of the pen. It is used to draw edges, text, etc. */
- Color currentPenColor;
- /**
- * The current color of the brush. It is used to draw the interior of
- * primitives.
- */
- Color currentBrushColor;
- /** The system name of the current font we are using (e.g.
- * "Times New Roman"). */
- Font currentFont;
- /** The stroke width to be used. */
- // This really needs to be a fixed width stroke as the basic stroke is
- // anti-aliased and gets too faint, but the piccolo fixed width stroke
- // is too buggy and generates missing initial moveto in path definition
- // errors with an IllegalPathStateException that cannot be caught because
- // it is in the automatic repaint function. If we can fix the exceptions
- // in piccolo, then we can use the following instead of BasicStroke:
- // import edu.umd.cs.piccolox.util.PFixedWidthStroke;
- // PFixedWidthStroke stroke = new PFixedWidthStroke(0.5f);
- // Instead we use the BasicStroke and turn off anti-aliasing.
- BasicStroke stroke = new BasicStroke(0.5f);
- /**
- * A unique representation for the window, also known by the client. It is
- * used when sending messages from server to client to identify him.
- */
- public int hash;
- /**
- * The total number of created Windows. If this ever reaches 0 (apart from the
- * beginning), quit the server.
- */
- public static int nrWindows = 0;
- /**
- * The Canvas, MessageBox, EventHandler, Menubar and Popupmenu associated with
- * this window.
- */
- private SVEventHandler svEventHandler = null;
- private SVMenuBar svMenuBar = null;
- private TextArea ta = null;
- public SVPopupMenu svPuMenu = null;
- public PCanvas canvas;
- private int winSizeX;
- private int winSizeY;
- /** Set the brush to an RGB color */
- public void brush(int red, int green, int blue) {
- brush(red, green, blue, 255);
- }
- /** Set the brush to an RGBA color */
- public void brush(int red, int green, int blue, int alpha) {
- // If alpha is zero, use a null brush to save rendering time.
- if (alpha == 0) {
- currentBrushColor = null;
- } else {
- currentBrushColor = new Color(red, green, blue, alpha);
- }
- }
- /** Erase all content from the window, but do not destroy it. */
- public void clear() {
- // Manipulation of Piccolo's scene graph should be done from Swings
- // event dispatch thread since Piccolo is not thread safe. This code calls
- // removeAllChildren() from that thread and releases the latch.
- final java.util.concurrent.CountDownLatch latch = new java.util.concurrent.CountDownLatch(1);
- SwingUtilities.invokeLater(new Runnable() {
- public void run() {
- layer.removeAllChildren();
- repaint();
- latch.countDown();
- }
- });
- try {
- latch.await();
- } catch (InterruptedException e) {
- }
- }
- /**
- * Start setting up a new polyline. The server will now expect
- * polyline data until the polyline is complete.
- *
- * @param length number of coordinate pairs
- */
- public void createPolyline(int length) {
- ScrollView.polylineXCoords = new float[length];
- ScrollView.polylineYCoords = new float[length];
- ScrollView.polylineSize = length;
- ScrollView.polylineScanned = 0;
- }
- /**
- * Draw the now complete polyline.
- */
- public void drawPolyline() {
- int numCoords = ScrollView.polylineXCoords.length;
- if (numCoords < 2) {
- return;
- }
- PPath pn = PPath.createLine(ScrollView.polylineXCoords[0],
- ScrollView.polylineYCoords[0],
- ScrollView.polylineXCoords[1],
- ScrollView.polylineYCoords[1]);
- pn.reset();
- pn.moveTo(ScrollView.polylineXCoords[0], ScrollView.polylineYCoords[0]);
- for (int p = 1; p < numCoords; ++p) {
- pn.lineTo(ScrollView.polylineXCoords[p], ScrollView.polylineYCoords[p]);
- }
- pn.closePath();
- ScrollView.polylineSize = 0;
- pn.setStrokePaint(currentPenColor);
- pn.setPaint(null); // Don't fill the polygon - this is just a polyline.
- pn.setStroke(stroke);
- layer.addChild(pn);
- }
- /**
- * Construct a new SVWindow and set it visible.
- *
- * @param name Title of the window.
- * @param hash Unique internal representation. This has to be the same as
- * defined by the client, as they use this to refer to the windows.
- * @param posX X position of where to draw the window (upper left).
- * @param posY Y position of where to draw the window (upper left).
- * @param sizeX The width of the window.
- * @param sizeY The height of the window.
- * @param canvasSizeX The canvas width of the window.
- * @param canvasSizeY The canvas height of the window.
- */
- public SVWindow(String name, int hash, int posX, int posY, int sizeX,
- int sizeY, int canvasSizeX, int canvasSizeY) {
- super(name);
- // Provide defaults for sizes.
- if (sizeX <= 0) sizeX = canvasSizeX;
- if (sizeY <= 0) sizeY = canvasSizeY;
- if (canvasSizeX <= 0) canvasSizeX = sizeX;
- if (canvasSizeY <= 0) canvasSizeY = sizeY;
- // Avoid later division by zero.
- if (sizeX <= 0) {
- sizeX = 1;
- canvasSizeX = sizeX;
- }
- if (sizeY <= 0) {
- sizeY = 1;
- canvasSizeY = sizeY;
- }
- // Initialize variables
- nrWindows++;
- this.hash = hash;
- this.svEventHandler = new SVEventHandler(this);
- this.currentPenColor = Color.BLACK;
- this.currentBrushColor = Color.BLACK;
- this.currentFont = new Font("Times New Roman", Font.PLAIN, 12);
- // Determine the initial size and zoom factor of the window.
- // If the window is too big, rescale it and zoom out.
- int shrinkfactor = 1;
- if (sizeX > MAX_WINDOW_X) {
- shrinkfactor = (sizeX + MAX_WINDOW_X - 1) / MAX_WINDOW_X;
- }
- if (sizeY / shrinkfactor > MAX_WINDOW_Y) {
- shrinkfactor = (sizeY + MAX_WINDOW_Y - 1) / MAX_WINDOW_Y;
- }
- winSizeX = sizeX / shrinkfactor;
- winSizeY = sizeY / shrinkfactor;
- double initialScalingfactor = 1.0 / shrinkfactor;
- if (winSizeX > canvasSizeX || winSizeY > canvasSizeY) {
- initialScalingfactor = Math.min(1.0 * winSizeX / canvasSizeX,
- 1.0 * winSizeY / canvasSizeY);
- }
- // Setup the actual window (its size, camera, title, etc.)
- if (canvas == null) {
- canvas = new PCanvas();
- getContentPane().add(canvas, BorderLayout.CENTER);
- }
- layer = canvas.getLayer();
- canvas.setBackground(Color.BLACK);
- // Disable antialiasing to make the lines more visible.
- canvas.setDefaultRenderQuality(PPaintContext.LOW_QUALITY_RENDERING);
- setLayout(new BorderLayout());
- setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
- validate();
- canvas.requestFocus();
- // Manipulation of Piccolo's scene graph should be done from Swings
- // event dispatch thread since Piccolo is not thread safe. This code calls
- // initialize() from that thread once the PFrame is initialized, so you are
- // safe to start working with Piccolo in the initialize() method.
- SwingUtilities.invokeLater(new Runnable() {
- public void run() {
- repaint();
- }
- });
- setSize(winSizeX, winSizeY);
- setLocation(posX, posY);
- setTitle(name);
- // Add a Scrollpane to be able to scroll within the canvas
- PScrollPane scrollPane = new PScrollPane(canvas);
- getContentPane().add(scrollPane);
- scrollPane.setWheelScrollingEnabled(false);
- PCamera lc = canvas.getCamera();
- lc.scaleViewAboutPoint(initialScalingfactor, 0, 0);
- // Disable the default event handlers and add our own.
- addWindowListener(svEventHandler);
- canvas.removeInputEventListener(canvas.getPanEventHandler());
- canvas.removeInputEventListener(canvas.getZoomEventHandler());
- canvas.addInputEventListener(svEventHandler);
- canvas.addKeyListener(svEventHandler);
- // Make the window visible.
- validate();
- setVisible(true);
- }
- /**
- * Convenience function to add a message box to the window which can be used
- * to output debug information.
- */
- public void addMessageBox() {
- if (ta == null) {
- ta = new TextArea();
- ta.setEditable(false);
- getContentPane().add(ta, BorderLayout.SOUTH);
- }
- // We need to make the window bigger to accommodate the message box.
- winSizeY += DEF_MESSAGEBOX_HEIGHT;
- setSize(winSizeX, winSizeY);
- }
- /**
- * Allows you to specify the thickness with which to draw lines, recantgles
- * and ellipses.
- * @param width The new thickness.
- */
- public void setStrokeWidth(float width) {
- // If this worked we wouldn't need the antialiased rendering off.
- // stroke = new PFixedWidthStroke(width);
- stroke = new BasicStroke(width);
- }
- /**
- * Draw an ellipse at (x,y) with given width and height, using the
- * current stroke, the current brush color to fill it and the
- * current pen color for the outline.
- */
- public void drawEllipse(int x, int y, int width, int height) {
- PPath pn = PPath.createEllipse(x, y, width, height);
- pn.setStrokePaint(currentPenColor);
- pn.setStroke(stroke);
- pn.setPaint(currentBrushColor);
- layer.addChild(pn);
- }
- /**
- * Draw the image with the given name at (x,y). Any image loaded stays in
- * memory, so if you intend to redraw an image, you do not have to use
- * createImage again.
- */
- public void drawImage(PImage img, int xPos, int yPos) {
- img.setX(xPos);
- img.setY(yPos);
- layer.addChild(img);
- }
- /**
- * Draw a line from (x1,y1) to (x2,y2) using the current pen color and stroke.
- */
- public void drawLine(int x1, int y1, int x2, int y2) {
- PPath pn = PPath.createLine(x1, y1, x2, y2);
- pn.setStrokePaint(currentPenColor);
- pn.setPaint(null); // Null paint may render faster than the default.
- pn.setStroke(stroke);
- pn.moveTo(x1, y1);
- pn.lineTo(x2, y2);
- layer.addChild(pn);
- }
- /**
- * Draw a rectangle given the two points (x1,y1) and (x2,y2) using the current
- * stroke, pen color for the border and the brush to fill the
- * interior.
- */
- public void drawRectangle(int x1, int y1, int x2, int y2) {
- if (x1 > x2) {
- int t = x1;
- x1 = x2;
- x2 = t;
- }
- if (y1 > y2) {
- int t = y1;
- y1 = y2;
- y2 = t;
- }
- PPath pn = PPath.createRectangle(x1, y1, x2 - x1, y2 - y1);
- pn.setStrokePaint(currentPenColor);
- pn.setStroke(stroke);
- pn.setPaint(currentBrushColor);
- layer.addChild(pn);
- }
- /**
- * Draw some text at (x,y) using the current pen color and text attributes. If
- * the current font does NOT support at least one character, it tries to find
- * a font which is capable of displaying it and use that to render the text.
- * Note: If the font says it can render a glyph, but in reality it turns out
- * to be crap, there is nothing we can do about it.
- */
- public void drawText(int x, int y, String text) {
- int unreadableCharAt = -1;
- char[] chars = text.toCharArray();
- PText pt = new PText(text);
- pt.setTextPaint(currentPenColor);
- pt.setFont(currentFont);
- // Check to see if every character can be displayed by the current font.
- for (int i = 0; i < chars.length; i++) {
- if (!currentFont.canDisplay(chars[i])) {
- // Set to the first not displayable character.
- unreadableCharAt = i;
- break;
- }
- }
- // Have to find some working font and use it for this text entry.
- if (unreadableCharAt != -1) {
- Font[] allfonts =
- GraphicsEnvironment.getLocalGraphicsEnvironment().getAllFonts();
- for (int j = 0; j < allfonts.length; j++) {
- if (allfonts[j].canDisplay(chars[unreadableCharAt])) {
- Font tempFont =
- new Font(allfonts[j].getFontName(), currentFont.getStyle(),
- currentFont.getSize());
- pt.setFont(tempFont);
- break;
- }
- }
- }
- pt.setX(x);
- pt.setY(y);
- layer.addChild(pt);
- }
- /** Set the pen color to an RGB value */
- public void pen(int red, int green, int blue) {
- pen(red, green, blue, 255);
- }
- /** Set the pen color to an RGBA value */
- public void pen(int red, int green, int blue, int alpha) {
- currentPenColor = new Color(red, green, blue, alpha);
- }
- /**
- * Define how to display text. Note: underlined is not currently not supported
- */
- public void textAttributes(String font, int pixelSize, boolean bold,
- boolean italic, boolean underlined) {
- // For legacy reasons convert "Times" to "Times New Roman"
- if (font.equals("Times")) {
- font = "Times New Roman";
- }
- int style = Font.PLAIN;
- if (bold) {
- style += Font.BOLD;
- }
- if (italic) {
- style += Font.ITALIC;
- }
- currentFont = new Font(font, style, pixelSize);
- }
- /**
- * Zoom the window to the rectangle given the two points (x1,y1)
- * and (x2,y2), which must be greater than (x1,y1).
- */
- public void zoomRectangle(int x1, int y1, int x2, int y2) {
- if (x2 > x1 && y2 > y1) {
- winSizeX = getWidth();
- winSizeY = getHeight();
- int width = x2 - x1;
- int height = y2 - y1;
- // Since piccolo doesn't do this well either, pad with a margin
- // all the way around.
- int wmargin = width / 2;
- int hmargin = height / 2;
- double scalefactor = Math.min(winSizeX / (2.0 * wmargin + width),
- winSizeY / (2.0 * hmargin + height));
- PCamera lc = canvas.getCamera();
- lc.scaleView(scalefactor / lc.getViewScale());
- lc.animateViewToPanToBounds(new Rectangle(x1 - hmargin, y1 - hmargin,
- 2 * wmargin + width,
- 2 * hmargin + height), 0);
- }
- }
- /**
- * Flush buffers and update display.
- *
- * Only actually reacts if there are no more messages in the stack, to prevent
- * the canvas from flickering.
- */
- public void update() {
- // TODO(rays) fix bugs in piccolo or use something else.
- // The repaint function generates many
- // exceptions for no good reason. We catch and ignore as many as we
- // can here, but most of them are generated by the system repaints
- // caused by resizing/exposing parts of the window etc, and they
- // generate unwanted stack traces that have to be piped to /dev/null
- // (on linux).
- try {
- repaint();
- } catch (NullPointerException e) {
- // Do nothing so the output isn't full of stack traces.
- } catch (IllegalPathStateException e) {
- // Do nothing so the output isn't full of stack traces.
- }
- }
- /** Adds a checkbox entry to the menubar, c.f. SVMenubar.add(...) */
- public void addMenuBarItem(String parent, String name, int id,
- boolean checked) {
- svMenuBar.add(parent, name, id, checked);
- }
- /** Adds a submenu to the menubar, c.f. SVMenubar.add(...) */
- public void addMenuBarItem(String parent, String name) {
- addMenuBarItem(parent, name, -1);
- }
- /** Adds a new entry to the menubar, c.f. SVMenubar.add(...) */
- public void addMenuBarItem(String parent, String name, int id) {
- if (svMenuBar == null) {
- svMenuBar = new SVMenuBar(this);
- }
- svMenuBar.add(parent, name, id);
- }
- /** Add a message to the message box. */
- public void addMessage(String message) {
- if (ta != null) {
- ta.append(message + "\n");
- } else {
- System.out.println(message + "\n");
- }
- }
- /**
- * This method converts a string which might contain hexadecimal values to a
- * string which contains the respective unicode counterparts.
- *
- * For example, Hall0x0094chen returns Hall<o umlaut>chen
- * encoded as utf8.
- *
- * @param input The original string, containing 0x values
- * @return The converted string which has the replaced unicode symbols
- */
- private static String convertIntegerStringToUnicodeString(String input) {
- StringBuffer sb = new StringBuffer(input);
- Pattern numbers = Pattern.compile("0x[0-9a-fA-F]{4}");
- Matcher matcher = numbers.matcher(sb);
- while (matcher.find()) {
- // Find the next match which resembles a hexadecimal value and convert it
- // to
- // its char value
- char a = (char) (Integer.decode(matcher.group()).intValue());
- // Replace the original with the new character
- sb.replace(matcher.start(), matcher.end(), String.valueOf(a));
- // Start again, since our positions have switched
- matcher.reset();
- }
- return sb.toString();
- }
- /**
- * Show a modal input dialog. The answer by the dialog is then send to the
- * client, together with the associated menu id, as SVET_POPUP
- *
- * @param msg The text that is displayed in the dialog.
- * @param def The default value of the dialog.
- * @param id The associated commandId
- * @param evtype The event this is associated with (usually SVET_MENU
- * or SVET_POPUP)
- */
- public void showInputDialog(String msg, String def, int id,
- SVEventType evtype) {
- svEventHandler.timer.stop();
- String tmp =
- (String) JOptionPane.showInputDialog(this, msg, "",
- JOptionPane.QUESTION_MESSAGE, null, null, def);
- if (tmp != null) {
- tmp = convertIntegerStringToUnicodeString(tmp);
- SVEvent res = new SVEvent(evtype, this, id, tmp);
- ScrollView.addMessage(res);
- }
- svEventHandler.timer.restart();
- }
- /**
- * Shows a modal input dialog to the user. The return value is automatically
- * sent to the client as SVET_INPUT event (with command id -1).
- *
- * @param msg The text of the dialog.
- */
- public void showInputDialog(String msg) {
- showInputDialog(msg, null, -1, SVEventType.SVET_INPUT);
- }
- /**
- * Shows a dialog presenting "Yes" and "No" as answers and returns either a
- * "y" or "n" to the client.
- *
- * Closing the dialog without answering is handled like "No".
- *
- * @param msg The text that is displayed in the dialog.
- */
- public void showYesNoDialog(String msg) {
- // res returns 0 on yes, 1 on no. Seems to be a bit counterintuitive
- int res =
- JOptionPane.showOptionDialog(this, msg, "", JOptionPane.YES_NO_OPTION,
- JOptionPane.QUESTION_MESSAGE, null, null, null);
- SVEvent e = new SVEvent(SVEventType.SVET_INPUT, this, 0, 0, 0, 0,
- res == 0 ? "y" : "n");
- ScrollView.addMessage(e);
- }
- /** Adds a submenu to the popup menu, c.f. SVPopupMenu.add(...) */
- public void addPopupMenuItem(String parent, String name) {
- if (svPuMenu == null) {
- svPuMenu = new SVPopupMenu(this);
- }
- svPuMenu.add(parent, name, -1);
- }
- /** Adds a new menu entry to the popup menu, c.f. SVPopupMenu.add(...) */
- public void addPopupMenuItem(String parent, String name, int cmdEvent,
- String value, String desc) {
- if (svPuMenu == null) {
- svPuMenu = new SVPopupMenu(this);
- }
- svPuMenu.add(parent, name, cmdEvent, value, desc);
- }
- /** Destroys a window. */
- public void destroy() {
- ScrollView.addMessage(new SVEvent(SVEventType.SVET_DESTROY, this, 0,
- "SVET_DESTROY"));
- setVisible(false);
- // dispose();
- }
- }
|