tesseract/java/com/google/scrollview/ui/SVWindow.java

641 lines
21 KiB
Java
Raw Normal View History

// 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 a 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;
// 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 anitaliasing 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 accomodate 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.
*
* @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 = null;
if (res == 0) {
e = new SVEvent(SVEventType.SVET_INPUT, this, 0, 0, 0, 0, "y");
} else if (res == 1) {
e = new SVEvent(SVEventType.SVET_INPUT, this, 0, 0, 0, 0, "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();
}
}