diff options
Diffstat (limited to 'tesseract/java/com/google/scrollview')
16 files changed, 2093 insertions, 0 deletions
diff --git a/tesseract/java/com/google/scrollview/Makefile.am b/tesseract/java/com/google/scrollview/Makefile.am new file mode 100644 index 00000000..7314b233 --- /dev/null +++ b/tesseract/java/com/google/scrollview/Makefile.am @@ -0,0 +1,4 @@ +SUBDIRS = events ui + +EXTRA_DIST = \ + ScrollView.java diff --git a/tesseract/java/com/google/scrollview/ScrollView.java b/tesseract/java/com/google/scrollview/ScrollView.java new file mode 100644 index 00000000..c3494af3 --- /dev/null +++ b/tesseract/java/com/google/scrollview/ScrollView.java @@ -0,0 +1,403 @@ +// 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; + +import com.google.scrollview.events.SVEvent; +import com.google.scrollview.ui.SVImageHandler; +import com.google.scrollview.ui.SVWindow; +import org.piccolo2d.nodes.PImage; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintStream; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.ArrayList; +import java.util.regex.Pattern; + +/** + * The ScrollView class is the main class which gets started from the command + * line. It sets up LUA and handles the network processing. + * @author wanke@google.com + */ +public class ScrollView { + + /** The port our server listens at. */ + public static int SERVER_PORT = 8461; + + /** + * All SVWindow objects share the same connection stream. The socket is needed + * to detect when the connection got closed, in/out are used to send and + * receive messages. + */ + private static Socket socket; + private static PrintStream out; + public static BufferedReader in; + public static float polylineXCoords[]; // The coords being received. + public static float polylineYCoords[]; // The coords being received. + public static int polylineSize; // The size of the coords arrays. + public static int polylineScanned; // The size read so far. + private static ArrayList<SVWindow> windows; // The id to SVWindow map. + private static Pattern intPattern; // For checking integer arguments. + private static Pattern floatPattern; // For checking float arguments. + + /** Keeps track of the number of messages received. */ + static int nrInputLines = 0; + + /** Prints all received messages to the console if true. */ + static boolean debugViewNetworkTraffic = false; + + /** Add a new message to the outgoing queue */ + public static void addMessage(SVEvent e) { + if (debugViewNetworkTraffic) { + System.out.println("(S->c) " + e.toString()); + } + String str = e.toString(); + // Send the whole thing as UTF8. + try { + byte [] utf8 = str.getBytes("UTF8"); + out.write(utf8, 0, utf8.length); + } catch (java.io.UnsupportedEncodingException ex) { + System.out.println("Oops... can't encode to UTF8... Exiting"); + System.exit(0); + } + out.println(); + // Flush the output and check for errors. + boolean error = out.checkError(); + if (error) { + System.out.println("Connection error. Quitting ScrollView Server..."); + System.exit(0); + } + } + + /** Read one message from client (assuming there are any). */ + public static String receiveMessage() throws IOException { + return in.readLine(); + } + + /** + * The main program loop. Basically loops trough receiving messages and + * processing them and then sending messages (if there are any). + */ + private static void IOLoop() { + String inputLine; + + try { + while (!socket.isClosed() && !socket.isInputShutdown() && + !socket.isOutputShutdown() && + socket.isConnected() && socket.isBound()) { + inputLine = receiveMessage(); + if (inputLine == null) { + // End of stream reached. + break; + } + nrInputLines++; + if (debugViewNetworkTraffic) { + System.out.println("(c->S," + nrInputLines + ")" + inputLine); + } + + if (polylineSize > polylineScanned) { + // We are processing a polyline. + // Read pairs of coordinates separated by commas. + boolean first = true; + for (String coordStr : inputLine.split(",")) { + int coord = Integer.parseInt(coordStr); + if (first) { + polylineXCoords[polylineScanned] = coord; + } else { + polylineYCoords[polylineScanned++] = coord; + } + first = !first; + } + assert first; + } else { + // Process this normally. + processInput(inputLine); + } + } + } + // Some connection error + catch (IOException e) { + System.out.println("Connection error. Quitting ScrollView Server..."); + } + System.exit(0); + } + + // Parse a comma-separated list of arguments into ArrayLists of the + // possible types. Each type is stored in order, but the order + // distinction between types is lost. + // Note that the format is highly constrained to what the client used + // to send to LUA: + // Quoted string -> String. + // true or false -> Boolean. + // %f format number -> Float (no %e allowed) + // Sequence of digits -> Integer + // Nothing else allowed. + private static void parseArguments(String argList, + ArrayList<Integer> intList, + ArrayList<Float> floatList, + ArrayList<String> stringList, + ArrayList<Boolean> boolList) { + // str is only non-null if an argument starts with a single or double + // quote. str is set back to null on completion of the string with a + // matching quote. If the string contains a comma then str will stay + // non-null across multiple argStr values until a matching closing quote. + // Backslash escaped quotes do not count as terminating the string. + String str = null; + for (String argStr : argList.split(",")) { + if (str != null) { + // Last string was incomplete. Append argStr to it and restore comma. + // Execute str += "," + argStr in Java. + int length = str.length() + 1 + argStr.length(); + StringBuilder appended = new StringBuilder(length); + appended.append(str); + appended.append(","); + appended.append(argStr); + str = appended.toString(); + } else if (argStr.length() == 0) { + continue; + } else { + char quote = argStr.charAt(0); + // If it begins with a quote then it is a string, but may not + // end this time if it contained a comma. + if (quote == '\'' || quote == '"') { + str = argStr; + } + } + if (str != null) { + // It began with a quote. Check that it still does. + assert str.charAt(0) == '\'' || str.charAt(0) == '"'; + int len = str.length(); + if (len > 1 && str.charAt(len - 1) == str.charAt(0)) { + // We have an ending quote of the right type. Now check that + // it is not escaped. Must have an even number of slashes before. + int slash = len - 1; + while (slash > 0 && str.charAt(slash - 1) == '\\') + --slash; + if ((len - 1 - slash) % 2 == 0) { + // It is now complete. Chop off the quotes and save. + // TODO(rays) remove the first backslash of each pair. + stringList.add(str.substring(1, len - 1)); + str = null; + } + } + // If str is not null here, then we have a string with a comma in it. + // Append , and the next argument at the next iteration, but check + // that str is null after the loop terminates in case it was an + // unterminated string. + } else if (floatPattern.matcher(argStr).matches()) { + // It is a float. + floatList.add(Float.parseFloat(argStr)); + } else if (argStr.equals("true")) { + boolList.add(true); + } else if (argStr.equals("false")) { + boolList.add(false); + } else if (intPattern.matcher(argStr).matches()) { + // Only contains digits so must be an int. + intList.add(Integer.parseInt(argStr)); + } + // else ignore all incompatible arguments for forward compatibility. + } + // All strings must have been terminated. + assert str == null; + } + + /** Executes the LUA command parsed as parameter. */ + private static void processInput(String inputLine) { + if (inputLine == null) { + return; + } + // Execute a function encoded as a LUA statement! Yuk! + if (inputLine.charAt(0) == 'w') { + // This is a method call on a window. Parse it. + String noWLine = inputLine.substring(1); + String[] idStrs = noWLine.split("[ :]", 2); + int windowID = Integer.parseInt(idStrs[0]); + // Find the parentheses. + int start = inputLine.indexOf('('); + int end = inputLine.lastIndexOf(')'); + // Parse the args. + ArrayList<Integer> intList = new ArrayList<Integer>(4); + ArrayList<Float> floatList = new ArrayList<Float>(2); + ArrayList<String> stringList = new ArrayList<String>(4); + ArrayList<Boolean> boolList = new ArrayList<Boolean>(3); + parseArguments(inputLine.substring(start + 1, end), + intList, floatList, stringList, boolList); + int colon = inputLine.indexOf(':'); + if (colon > 1 && colon < start) { + // This is a regular function call. Look for the name and call it. + String func = inputLine.substring(colon + 1, start); + if (func.equals("drawLine")) { + windows.get(windowID).drawLine(intList.get(0), intList.get(1), + intList.get(2), intList.get(3)); + } else if (func.equals("createPolyline")) { + windows.get(windowID).createPolyline(intList.get(0)); + } else if (func.equals("drawPolyline")) { + windows.get(windowID).drawPolyline(); + } else if (func.equals("drawRectangle")) { + windows.get(windowID).drawRectangle(intList.get(0), intList.get(1), + intList.get(2), intList.get(3)); + } else if (func.equals("setVisible")) { + windows.get(windowID).setVisible(boolList.get(0)); + } else if (func.equals("setAlwaysOnTop")) { + windows.get(windowID).setAlwaysOnTop(boolList.get(0)); + } else if (func.equals("addMessage")) { + windows.get(windowID).addMessage(stringList.get(0)); + } else if (func.equals("addMessageBox")) { + windows.get(windowID).addMessageBox(); + } else if (func.equals("clear")) { + windows.get(windowID).clear(); + } else if (func.equals("setStrokeWidth")) { + windows.get(windowID).setStrokeWidth(floatList.get(0)); + } else if (func.equals("drawEllipse")) { + windows.get(windowID).drawEllipse(intList.get(0), intList.get(1), + intList.get(2), intList.get(3)); + } else if (func.equals("pen")) { + if (intList.size() == 4) { + windows.get(windowID).pen(intList.get(0), intList.get(1), + intList.get(2), intList.get(3)); + } else { + windows.get(windowID).pen(intList.get(0), intList.get(1), + intList.get(2)); + } + } else if (func.equals("brush")) { + if (intList.size() == 4) { + windows.get(windowID).brush(intList.get(0), intList.get(1), + intList.get(2), intList.get(3)); + } else { + windows.get(windowID).brush(intList.get(0), intList.get(1), + intList.get(2)); + } + } else if (func.equals("textAttributes")) { + windows.get(windowID).textAttributes(stringList.get(0), + intList.get(0), + boolList.get(0), + boolList.get(1), + boolList.get(2)); + } else if (func.equals("drawText")) { + windows.get(windowID).drawText(intList.get(0), intList.get(1), + stringList.get(0)); + } else if (func.equals("addMenuBarItem")) { + if (boolList.size() > 0) { + windows.get(windowID).addMenuBarItem(stringList.get(0), + stringList.get(1), + intList.get(0), + boolList.get(0)); + } else if (intList.size() > 0) { + windows.get(windowID).addMenuBarItem(stringList.get(0), + stringList.get(1), + intList.get(0)); + } else { + windows.get(windowID).addMenuBarItem(stringList.get(0), + stringList.get(1)); + } + } else if (func.equals("addPopupMenuItem")) { + if (stringList.size() == 4) { + windows.get(windowID).addPopupMenuItem(stringList.get(0), + stringList.get(1), + intList.get(0), + stringList.get(2), + stringList.get(3)); + } else { + windows.get(windowID).addPopupMenuItem(stringList.get(0), + stringList.get(1)); + } + } else if (func.equals("update")) { + windows.get(windowID).update(); + } else if (func.equals("showInputDialog")) { + windows.get(windowID).showInputDialog(stringList.get(0)); + } else if (func.equals("showYesNoDialog")) { + windows.get(windowID).showYesNoDialog(stringList.get(0)); + } else if (func.equals("zoomRectangle")) { + windows.get(windowID).zoomRectangle(intList.get(0), intList.get(1), + intList.get(2), intList.get(3)); + } else if (func.equals("readImage")) { + PImage image = SVImageHandler.readImage(intList.get(2), in); + windows.get(windowID).drawImage(image, intList.get(0), intList.get(1)); + } else if (func.equals("drawImage")) { + PImage image = new PImage(stringList.get(0)); + windows.get(windowID).drawImage(image, intList.get(0), intList.get(1)); + } else if (func.equals("destroy")) { + windows.get(windowID).destroy(); + } + // else for forward compatibility purposes, silently ignore any + // unrecognized function call. + } else { + // No colon. Check for create window. + if (idStrs[1].startsWith("= luajava.newInstance")) { + while (windows.size() <= windowID) { + windows.add(null); + } + windows.set(windowID, new SVWindow(stringList.get(1), + intList.get(0), intList.get(1), + intList.get(2), intList.get(3), + intList.get(4), intList.get(5), + intList.get(6))); + } + // else for forward compatibility purposes, silently ignore any + // unrecognized function call. + } + } else if (inputLine.startsWith("svmain")) { + // Startup or end. Startup is a lua bind, which is now a no-op. + if (inputLine.startsWith("svmain:exit")) { + exit(); + } + // else for forward compatibility purposes, silently ignore any + // unrecognized function call. + } + // else for forward compatibility purposes, silently ignore any + // unrecognized function call. + } + + /** Called from the client to make the server exit. */ + public static void exit() { + System.exit(0); + } + + /** + * The main function. Sets up LUA and the server connection and then calls the + * IOLoop. + */ + public static void main(String[] args) { + if (args.length > 0) { + SERVER_PORT = Integer.parseInt(args[0]); + } + windows = new ArrayList<SVWindow>(100); + intPattern = Pattern.compile("[0-9-][0-9]*"); + floatPattern = Pattern.compile("[0-9-][0-9]*\\.[0-9]*"); + + // Open a socket to listen on. + try (ServerSocket serverSocket = new ServerSocket(SERVER_PORT)) { + System.out.println("Socket started on port " + SERVER_PORT); + + // Wait (blocking) for an incoming connection + socket = serverSocket.accept(); + System.out.println("Client connected"); + + // Setup the streams + out = new PrintStream(socket.getOutputStream(), true, "UTF-8"); + in = + new BufferedReader(new InputStreamReader(socket.getInputStream(), + "UTF8")); + } catch (IOException e) { + // Something went wrong and we were unable to set up a connection. This is + // pretty + // much a fatal error. + // Note: The server does not get restarted automatically if this happens. + e.printStackTrace(); + System.exit(1); + } + + // Enter the main program loop. + IOLoop(); + } +} diff --git a/tesseract/java/com/google/scrollview/events/Makefile.am b/tesseract/java/com/google/scrollview/events/Makefile.am new file mode 100644 index 00000000..ec3a21ff --- /dev/null +++ b/tesseract/java/com/google/scrollview/events/Makefile.am @@ -0,0 +1,5 @@ +SUBDIRS = + +EXTRA_DIST = \ + SVEvent.java SVEventHandler.java \ + SVEventType.java diff --git a/tesseract/java/com/google/scrollview/events/SVEvent.java b/tesseract/java/com/google/scrollview/events/SVEvent.java new file mode 100644 index 00000000..18309c2f --- /dev/null +++ b/tesseract/java/com/google/scrollview/events/SVEvent.java @@ -0,0 +1,87 @@ +// 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.events; + +import com.google.scrollview.ui.SVWindow; + +/** + * The SVEvent is a structure which holds the actual values of a message to be + * transmitted. It corresponds to the client structure defined in scrollview.h + * + * @author wanke@google.com + */ +public class SVEvent { + SVEventType type; // What kind of event. + SVWindow window; // Window event relates to. + int x; // Coords of click or selection. + int y; + int xSize; // Size of selection. + int ySize; + int commandId; + String parameter; // Any string that might have been passed as argument. + + /** + * A "normal" SVEvent. + * + * @param t The type of the event as specified in SVEventType (e.g. + * SVET_CLICK) + * @param w The window the event corresponds to + * @param x1 X position of the mouse at the time of the event + * @param y1 Y position of the mouse at the time of the event + * @param x2 X selection size at the time of the event + * @param y2 Y selection size at the time of the event + * @param p A parameter associated with the event (e.g. keyboard input) + */ + public SVEvent(SVEventType t, SVWindow w, int x1, int y1, int x2, int y2, + String p) { + type = t; + window = w; + x = x1; + y = y1; + xSize = x2; + ySize = y2; + commandId = 0; + parameter = p; + } + + /** + * An event which issues a command (like clicking on a item in the menubar). + * + * @param eventtype The type of the event as specified in SVEventType + * (usually SVET_MENU or SVET_POPUP) + * @param svWindow The window the event corresponds to + * @param commandid The associated id with the command (given by the client + * on construction of the item) + * @param value A parameter associated with the event (e.g. keyboard input) + */ + public SVEvent(SVEventType eventtype, SVWindow svWindow, int commandid, + String value) { + type = eventtype; + window = svWindow; + + parameter = value; + x = 0; + y = 0; + xSize = 0; + ySize = 0; + commandId = commandid; + } + + /** + * This is the string representation of the message, which is what will + * actually be transferred over the network. + */ + @Override + public String toString() { + return (window.hash + "," + type.ordinal() + "," + x + "," + y + "," + + xSize + "," + ySize + "," + commandId + "," + parameter); + } +} diff --git a/tesseract/java/com/google/scrollview/events/SVEventHandler.java b/tesseract/java/com/google/scrollview/events/SVEventHandler.java new file mode 100644 index 00000000..26a92bdb --- /dev/null +++ b/tesseract/java/com/google/scrollview/events/SVEventHandler.java @@ -0,0 +1,301 @@ +// 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.events; + +import com.google.scrollview.ScrollView; +import com.google.scrollview.events.SVEvent; +import com.google.scrollview.events.SVEventType; +import com.google.scrollview.ui.SVWindow; + +import org.piccolo2d.PCamera; +import org.piccolo2d.PNode; +import org.piccolo2d.event.PBasicInputEventHandler; +import org.piccolo2d.event.PInputEvent; +import org.piccolo2d.nodes.PPath; + +import java.awt.Color; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.awt.event.WindowEvent; +import java.awt.event.WindowListener; +import java.awt.Window; + +import javax.swing.Timer; + +/** + * The ScrollViewEventHandler takes care of any events which might happen on the + * canvas and converts them to an according SVEvent, which is (using the + * processEvent method) then added to a message queue. All events from the + * message queue get sent gradually + * + * @author wanke@google.com + */ +public class SVEventHandler extends PBasicInputEventHandler implements + ActionListener, KeyListener, WindowListener { + + /** Necessary to wait for a defined period of time (for SVET_HOVER). */ + public Timer timer; + + /** The window which the event corresponds to. */ + private SVWindow svWindow; + + /** These are used to determine a selection size (for SVET_SELECTION). */ + private int lastX = 0; + private int lastY = 0; + + /** + * These are used in case we want to transmit our position, but do not get it + * because it was no MouseEvent, in particular SVET_HOVER and SVET_INPUT. + */ + private int lastXMove = 0; + private int lastYMove = 0; + + /** For Drawing a rubber-band rectangle for selection */ + private int startX = 0; + private int startY = 0; + private float rubberBandTransparency = 0.5f; + private PNode selection = null; + + /** The string entered since the last enter. Since the client + * end eats all newlines, we can't use the newline + * character, so use ! for now, as it cannot be entered + * directly anyway and therefore can never show up for real. */ + private String keyStr = "!"; + + /** Setup the timer. */ + public SVEventHandler(SVWindow wdw) { + timer = new Timer(1000, this); + svWindow = wdw; + } + + /** + * Store the newest x,y values, add the message to the queue and restart the + * timer. + */ + private void processEvent(SVEvent e) { + lastXMove = e.x; + lastYMove = e.y; + ScrollView.addMessage(e); + timer.restart(); + } + + /** Show the associated popup menu at (x,y) (relative position of the window). */ + private void showPopup(PInputEvent e) { + double x = e.getCanvasPosition().getX(); + double y = e.getCanvasPosition().getY(); + + if (svWindow.svPuMenu != null) { + svWindow.svPuMenu.show(svWindow, (int) x, (int) y); + } + } + + + /** The mouse is clicked - create an SVET_CLICK event. */ + @Override + public void mouseClicked(PInputEvent e) { + if (e.isPopupTrigger()) { + showPopup(e); + } else { + processEvent(new SVEvent(SVEventType.SVET_CLICK, svWindow, (int) e + .getPosition().getX(), (int) e.getPosition().getY(), 0, 0, null)); + } + } + + /** + * The mouse key is pressed (and keeps getting pressed). + * Depending on the OS, show a popup menu (if the button pressed is associated + * with popup menus, like the RMB under windows&linux) or otherwise save the + * position (in case it is a selection). + */ + @Override + public void mousePressed(PInputEvent e) { + if (e.isPopupTrigger()) { + showPopup(e); + } else { + lastX = (int) e.getPosition().getX(); + lastY = (int) e.getPosition().getY(); + timer.restart(); + } + } + + /** The mouse is getting dragged - create an SVET_MOUSE event. */ + @Override + public void mouseDragged(PInputEvent e) { + processEvent(new SVEvent(SVEventType.SVET_MOUSE, svWindow, (int) e + .getPosition().getX(), (int) e.getPosition().getY(), (int) e + .getPosition().getX() + - lastX, (int) e.getPosition().getY() - lastY, null)); + + // Paint a selection rectangle. + if (selection == null) { + startX = (int) e.getPosition().getX(); + startY = (int) e.getPosition().getY(); + selection = PPath.createRectangle(startX, startY, 1, 1); + selection.setTransparency(rubberBandTransparency); + svWindow.canvas.getLayer().addChild(selection); + } else { + int right = Math.max(startX, (int) e.getPosition().getX()); + int left = Math.min(startX, (int) e.getPosition().getX()); + int bottom = Math.max(startY, (int) e.getPosition().getY()); + int top = Math.min(startY, (int) e.getPosition().getY()); + svWindow.canvas.getLayer().removeChild(selection); + selection = PPath.createRectangle(left, top, right - left, bottom - top); + selection.setPaint(Color.YELLOW); + selection.setTransparency(rubberBandTransparency); + svWindow.canvas.getLayer().addChild(selection); + } + } + + /** + * The mouse was released. + * Depending on the OS, show a popup menu (if the button pressed is associated + * with popup menus, like the RMB under windows&linux) or otherwise create an + * SVET_SELECTION event. + */ + @Override + public void mouseReleased(PInputEvent e) { + if (e.isPopupTrigger()) { + showPopup(e); + } else { + processEvent(new SVEvent(SVEventType.SVET_SELECTION, svWindow, (int) e + .getPosition().getX(), (int) e.getPosition().getY(), (int) e + .getPosition().getX() + - lastX, (int) e.getPosition().getY() - lastY, null)); + } + if (selection != null) { + svWindow.canvas.getLayer().removeChild(selection); + selection = null; + } + } + + /** + * The mouse wheel is used to zoom in and out of the viewport and center on + * the (x,y) position the mouse is currently on. + */ + @Override + public void mouseWheelRotated(PInputEvent e) { + PCamera lc = svWindow.canvas.getCamera(); + double sf = SVWindow.SCALING_FACTOR; + + if (e.getWheelRotation() < 0) { + sf = 1 / sf; + } + lc.scaleViewAboutPoint(lc.getScale() / sf, e.getPosition().getX(), e + .getPosition().getY()); + } + + /** + * The mouse was moved - create an SVET_MOTION event. NOTE: This obviously + * creates a lot of traffic and, depending on the type of application, could + * quite possibly be disabled. + */ + @Override + public void mouseMoved(PInputEvent e) { + processEvent(new SVEvent(SVEventType.SVET_MOTION, svWindow, (int) e + .getPosition().getX(), (int) e.getPosition().getY(), 0, 0, null)); + } + + /** + * The mouse entered the window. + * Start the timer, which will then emit SVET_HOVER events every X ms. */ + @Override + public void mouseEntered(PInputEvent e) { + timer.restart(); + } + + /** + * The mouse exited the window + * Stop the timer, so no more SVET_HOVER events will emit. */ + @Override + public void mouseExited(PInputEvent e) { + timer.stop(); + } + + /** + * The only associated object with this is the timer, so we use it to send a + * SVET_HOVER event. + */ + public void actionPerformed(ActionEvent e) { + processEvent(new SVEvent(SVEventType.SVET_HOVER, svWindow, lastXMove, + lastYMove, 0, 0, null)); + } + + /** + * A key was pressed - create an SVET_INPUT event. + * + * NOTE: Might be useful to specify hotkeys. + * + * Implementation note: The keyListener provided by Piccolo seems to be + * broken, so we use the AWT listener directly. + * There are never any keyTyped events received either so we are + * stuck with physical keys, which is very ugly. + */ + public void keyPressed(KeyEvent e) { + char keyCh = e.getKeyChar(); + if (keyCh == '\r' || keyCh == '\n' || keyCh == '\0' || keyCh == '?') { + processEvent(new SVEvent(SVEventType.SVET_INPUT, svWindow, lastXMove, + lastYMove, 0, 0, keyStr)); + // Send newline characters as '!' as '!' can never be a keypressed + // and the client eats all newline characters. + keyStr = "!"; + } else { + processEvent(new SVEvent(SVEventType.SVET_INPUT, svWindow, lastXMove, + lastYMove, 0, 0, String.valueOf(keyCh))); + keyStr += keyCh; + } + } + + /** + * A window is closed (by the 'x') - create an SVET_DESTROY event. If it was + * the last open Window, also send an SVET_EXIT event (but do not exit unless + * the client says so). + */ + public void windowClosing(WindowEvent e) { + processEvent(new SVEvent(SVEventType.SVET_DESTROY, svWindow, lastXMove, + lastYMove, 0, 0, null)); + Window w = e.getWindow(); + if (w != null) { + w.dispose(); + } + SVWindow.nrWindows--; + if (SVWindow.nrWindows == 0) { + processEvent(new SVEvent(SVEventType.SVET_EXIT, svWindow, lastXMove, + lastYMove, 0, 0, null)); + } + } + + /** These are all events we do not care about and throw away */ + public void keyReleased(KeyEvent e) { + } + + public void keyTyped(KeyEvent e) { + } + + public void windowActivated(WindowEvent e) { + } + + public void windowClosed(WindowEvent e) { + } + + public void windowDeactivated(WindowEvent e) { + } + + public void windowDeiconified(WindowEvent e) { + } + + public void windowIconified(WindowEvent e) { + } + + public void windowOpened(WindowEvent e) { + } +} diff --git a/tesseract/java/com/google/scrollview/events/SVEventType.java b/tesseract/java/com/google/scrollview/events/SVEventType.java new file mode 100644 index 00000000..b15f37e2 --- /dev/null +++ b/tesseract/java/com/google/scrollview/events/SVEventType.java @@ -0,0 +1,31 @@ +// 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.events; + +/** + * These are the defined events which can happen in ScrollView and be + * transferred to the client. They are same events as on the client side part of + * ScrollView (defined in ScrollView.h). + * + * @author wanke@google.com + */ +public enum SVEventType { + SVET_DESTROY, // Window has been destroyed by user. + SVET_EXIT, // User has destroyed the last window by clicking on the 'X' + SVET_CLICK, // Any button pressed that is not a popup trigger. + SVET_SELECTION, // Left button selection. + SVET_INPUT, // Any kind of input + SVET_MOUSE, // The mouse has moved with a button pressed. + SVET_MOTION, // The mouse has moved with no button pressed. + SVET_HOVER, // The mouse has stayed still for a second. + SVET_POPUP, // A command selected through a popup menu + SVET_MENU; // A command selected through the menubar +} diff --git a/tesseract/java/com/google/scrollview/ui/Makefile.am b/tesseract/java/com/google/scrollview/ui/Makefile.am new file mode 100644 index 00000000..556d8015 --- /dev/null +++ b/tesseract/java/com/google/scrollview/ui/Makefile.am @@ -0,0 +1,7 @@ +SUBDIRS = + +EXTRA_DIST = \ + SVAbstractMenuItem.java \ + SVCheckboxMenuItem.java SVEmptyMenuItem.java \ + SVImageHandler.java SVMenuBar.java \ + SVMenuItem.java SVPopupMenu.java SVSubMenuItem.java SVWindow.java diff --git a/tesseract/java/com/google/scrollview/ui/SVAbstractMenuItem.java b/tesseract/java/com/google/scrollview/ui/SVAbstractMenuItem.java new file mode 100644 index 00000000..a5c9c92d --- /dev/null +++ b/tesseract/java/com/google/scrollview/ui/SVAbstractMenuItem.java @@ -0,0 +1,57 @@ +// 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; + +/** + * A MenuListItem is any sort of menu entry. This can either be within a popup + * menu or within a menubar. It can either be a submenu (only name and + * command-id) or a name with an associated value and possibly description. They + * can also have new entries added (if they are submenus). + * + * @author wanke@google.com + */ + +import com.google.scrollview.events.SVEventType; + +import javax.swing.JMenu; +import javax.swing.JMenuItem; + +abstract class SVAbstractMenuItem { + JMenuItem mi; + public String name; + public int id; + + /** + * Sets the basic attributes for name, id and the corresponding swing item + */ + SVAbstractMenuItem(int id, String name, JMenuItem jmi) { + this.mi = jmi; + this.name = name; + this.id = id; + } + + /** Returns the actual value of the MenuListItem. */ + public String getValue() { return null; } + + /** Adds a child entry to the submenu. */ + public void add(SVAbstractMenuItem mli) { } + + /** Adds a child menu to the submenu (or root node). */ + public void add(JMenu jli) { } + + /** + * What to do when user clicks on this item. + * @param window The window the event happened. + * @param eventType What kind of event will be associated + * (usually SVET_POPUP or SVET_MENU). + */ + public void performAction(SVWindow window, SVEventType eventType) {} +} diff --git a/tesseract/java/com/google/scrollview/ui/SVCheckboxMenuItem.java b/tesseract/java/com/google/scrollview/ui/SVCheckboxMenuItem.java new file mode 100644 index 00000000..fa2d8323 --- /dev/null +++ b/tesseract/java/com/google/scrollview/ui/SVCheckboxMenuItem.java @@ -0,0 +1,57 @@ +// 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; + +/** + * A MenuListItem is any sort of menu entry. This can either be within a popup + * menu or within a menubar. It can either be a submenu (only name and + * command-id) or a name with an associated value and possibly description. They + * can also have new entries added (if they are submenus). + * + * @author wanke@google.com + */ + +import com.google.scrollview.ScrollView; +import com.google.scrollview.events.SVEvent; +import com.google.scrollview.events.SVEventType; + +import javax.swing.JCheckBoxMenuItem; + +/** + * Constructs a new menulistitem which possesses a flag that can be toggled. + */ +class SVCheckboxMenuItem extends SVAbstractMenuItem { + public boolean bvalue; + + SVCheckboxMenuItem(int id, String name, boolean val) { + super(id, name, new JCheckBoxMenuItem(name, val)); + bvalue = val; + } + + /** What to do when user clicks on this item. */ + @Override + public void performAction(SVWindow window, SVEventType eventType) { + // Checkbox entry - trigger and send event. + if (bvalue) { + bvalue = false; + } else { + bvalue = true; + } + SVEvent svme = new SVEvent(eventType, window, id, getValue()); + ScrollView.addMessage(svme); + } + + /** Returns the actual value of the MenuListItem. */ + @Override + public String getValue() { + return Boolean.toString(bvalue); + } +} diff --git a/tesseract/java/com/google/scrollview/ui/SVEmptyMenuItem.java b/tesseract/java/com/google/scrollview/ui/SVEmptyMenuItem.java new file mode 100644 index 00000000..950bcb5b --- /dev/null +++ b/tesseract/java/com/google/scrollview/ui/SVEmptyMenuItem.java @@ -0,0 +1,46 @@ +// 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; + +/** + * A MenuListItem is any sort of menu entry. This can either be within a popup + * menu or within a menubar. It can either be a submenu (only name and + * command-id) or a name with an associated value and possibly description. They + * can also have new entries added (if they are submenus). + * + * @author wanke@google.com + */ + +import com.google.scrollview.ScrollView; +import com.google.scrollview.events.SVEvent; +import com.google.scrollview.events.SVEventType; + +import javax.swing.JMenuItem; + +/** + * Constructs a new menulistitem which just has an ID and a name attached to + * it. In this case, we will have to ask for the value of the item and its + * description if it gets called. + */ +class SVEmptyMenuItem extends SVAbstractMenuItem { + SVEmptyMenuItem(int id, String name) { + super(id, name, new JMenuItem(name)); + } + /** What to do when user clicks on this item. */ + @Override + public void performAction(SVWindow window, SVEventType eventType) { + // Send an event indicating that someone clicked on an entry. + // Value will be null here. + SVEvent svme = + new SVEvent(eventType, window, id, getValue()); + ScrollView.addMessage(svme); + } +} diff --git a/tesseract/java/com/google/scrollview/ui/SVImageHandler.java b/tesseract/java/com/google/scrollview/ui/SVImageHandler.java new file mode 100644 index 00000000..ed6b7c04 --- /dev/null +++ b/tesseract/java/com/google/scrollview/ui/SVImageHandler.java @@ -0,0 +1,74 @@ +// 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 org.piccolo2d.nodes.PImage; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import javax.imageio.ImageIO; +import javax.xml.bind.DatatypeConverter; + +/** + * The ScrollViewImageHandler is a helper class which takes care of image + * processing. It is used to construct an Image from the message-stream and + * basically consists of a number of utility functions to process the input + * stream. + * + * @author wanke@google.com + */ +public class SVImageHandler { + /* All methods are static, so we forbid to construct SVImageHandler objects */ + private SVImageHandler() { + } + + /** + * Reads size bytes from the stream in and interprets it as an image file, + * encoded as png, and then text-encoded as base 64, returning the decoded + * bitmap. + * + * @param size The size of the image file. + * @param in The input stream from which to read the bytes. + */ + public static PImage readImage(int size, BufferedReader in) { + char[] charbuffer = new char[size]; + int numRead = 0; + while (numRead < size) { + int newRead = -1; + try { + newRead = in.read(charbuffer, numRead, size - numRead); + } catch (IOException e) { + System.out.println("Failed to read image data from socket:" + e.getMessage()); + return null; + } + if (newRead < 0) { + return null; + } + numRead += newRead; + } + if (numRead != size) { + System.out.println("Failed to read image data from socket"); + return null; + } + // Convert the character data to binary. + byte[] binarydata = DatatypeConverter.parseBase64Binary(new String(charbuffer)); + // Convert the binary data to a byte stream and parse to image. + ByteArrayInputStream byteStream = new ByteArrayInputStream(binarydata); + try { + PImage img = new PImage(ImageIO.read(byteStream)); + return img; + } catch (IOException e) { + System.out.println("Failed to decode image data from socket" + e.getMessage()); + } + return null; + } +} diff --git a/tesseract/java/com/google/scrollview/ui/SVMenuBar.java b/tesseract/java/com/google/scrollview/ui/SVMenuBar.java new file mode 100644 index 00000000..9a87524e --- /dev/null +++ b/tesseract/java/com/google/scrollview/ui/SVMenuBar.java @@ -0,0 +1,130 @@ +// 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.events.SVEventType; +import com.google.scrollview.ui.SVWindow; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.HashMap; + +import javax.swing.JMenu; +import javax.swing.JMenuBar; + +/** + * The SVMenuBar class provides the functionality to add a menubar to + * ScrollView. Each menubar item gets associated with a (client-defined) + * command-id, which SVMenuBar will return upon clicking it. + * + * @author wanke@google.com + * + */ +public class SVMenuBar implements ActionListener { + /** The root entry to add items to. */ + private JMenuBar root; + /** Contains a map of item name to its actual entry. */ + private HashMap<String, SVAbstractMenuItem> items; + /** The window the menubar belongs to. */ + private SVWindow svWindow; + + /** + * Create a new SVMenuBar and place it at the top of the ScrollView window. + * + * @param scrollView The window our menubar belongs to. + */ + public SVMenuBar(SVWindow scrollView) { + root = new JMenuBar(); + svWindow = scrollView; + items = new HashMap<String, SVAbstractMenuItem>(); + svWindow.setJMenuBar(root); + } + + + /** + * A click on one of the items in our menubar has occurred. Forward it + * to the item itself to let it decide what happens. + */ + public void actionPerformed(ActionEvent e) { + // Get the corresponding menuitem. + SVAbstractMenuItem svm = items.get(e.getActionCommand()); + + svm.performAction(svWindow, SVEventType.SVET_MENU); + } + + /** + * Add a new entry to the menubar. + * + * @param parent The menu we add our new entry to (should have been defined + * before). If the parent is "", we will add the entry to the root + * (top-level) + * @param name The caption of the new entry. + * @param id The Id of the new entry. If it is -1, the entry will be treated + * as a menu. + */ + public void add(String parent, String name, int id) { + // A duplicate entry - we just throw it away, since its already in. + if (items.get(name) != null) { return; } + // A new submenu at the top-level + if (parent.equals("")) { + JMenu jli = new JMenu(name); + SVAbstractMenuItem mli = new SVSubMenuItem(name, jli); + items.put(name, mli); + root.add(jli); + } + // A new sub-submenu + else if (id == -1) { + SVAbstractMenuItem jmi = items.get(parent); + JMenu jli = new JMenu(name); + SVAbstractMenuItem mli = new SVSubMenuItem(name, jli); + items.put(name, mli); + jmi.add(jli); + } + // A new child entry. Add to appropriate parent. + else { + SVAbstractMenuItem jmi = items.get(parent); + if (jmi == null) { + System.out.println("ERROR: Unknown parent " + parent); + System.exit(1); + } + SVAbstractMenuItem mli = new SVEmptyMenuItem(id, name); + mli.mi.addActionListener(this); + items.put(name, mli); + jmi.add(mli); + } + } + + /** + * Add a new checkbox entry to the menubar. + * + * @param parent The menu we add our new entry to (should have been defined + * before). If the parent is "", we will add the entry to the root + * (top-level) + * @param name The caption of the new entry. + * @param id The Id of the new entry. If it is -1, the entry will be treated + * as a menu. + * @param b Whether the entry is initially flagged. + * + */ + + public void add(String parent, String name, int id, boolean b) { + SVAbstractMenuItem jmi = items.get(parent); + if (jmi == null) { + System.out.println("ERROR: Unknown parent " + parent); + System.exit(1); + } + SVAbstractMenuItem mli = new SVCheckboxMenuItem(id, name, b); + mli.mi.addActionListener(this); + items.put(name, mli); + jmi.add(mli); + } + +} diff --git a/tesseract/java/com/google/scrollview/ui/SVMenuItem.java b/tesseract/java/com/google/scrollview/ui/SVMenuItem.java new file mode 100644 index 00000000..932ab7cc --- /dev/null +++ b/tesseract/java/com/google/scrollview/ui/SVMenuItem.java @@ -0,0 +1,60 @@ +// 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; + +/** + * A MenuListItem is any sort of menu entry. This can either be within a popup + * menu or within a menubar. It can either be a submenu (only name and + * command-id) or a name with an associated value and possibly description. They + * can also have new entries added (if they are submenus). + * + * @author wanke@google.com + */ + +import com.google.scrollview.events.SVEventType; + +import javax.swing.JMenuItem; + +/** + * Constructs a new menulistitem which also has a value and a description. For + * these, we will not have to ask the server what the value is when the user + * wants to change it, but can just call the client with the new value. + */ +class SVMenuItem extends SVAbstractMenuItem { + public String value = null; + public String desc = null; + + SVMenuItem(int id, String name, String v, String d) { + super(id, name, new JMenuItem(name)); + value = v; + desc = d; + } + + /** + * Ask the user for new input for a variable and send it. + * Depending on whether there is a description given for the entry, show + * the description in the dialog or just show the name. + */ + @Override + public void performAction(SVWindow window, SVEventType eventType) { + if (desc != null) { + window.showInputDialog(desc, value, id, eventType); + } else { + window.showInputDialog(name, value, id, eventType); + } + } + + /** Returns the actual value of the MenuListItem. */ + @Override + public String getValue() { + return value; + } +} diff --git a/tesseract/java/com/google/scrollview/ui/SVPopupMenu.java b/tesseract/java/com/google/scrollview/ui/SVPopupMenu.java new file mode 100644 index 00000000..14c8b3ac --- /dev/null +++ b/tesseract/java/com/google/scrollview/ui/SVPopupMenu.java @@ -0,0 +1,144 @@ +// 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.events.SVEventType; +import com.google.scrollview.ui.SVMenuItem; +import com.google.scrollview.ui.SVWindow; + +import java.awt.Component; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.HashMap; + +import javax.swing.JMenu; +import javax.swing.JPopupMenu; + +/** + * The SVPopupMenu class provides the functionality to add a popup menu to + * ScrollView. Each popup menu item gets associated with a (client-defined) + * command-id, which SVPopupMenu will return upon clicking it. + * + * @author wanke@google.com + * + */ + +public class SVPopupMenu implements ActionListener { + /** The root entry to add items to. */ + private JPopupMenu root; + /** Contains a map of item name to its actual entry. */ + private HashMap<String, SVAbstractMenuItem> items; + /** The window the menubar belongs to. */ + private SVWindow svWindow; + + /** + * Create a new SVPopupMenu and associate it with a ScrollView window. + * + * @param sv The window our popup menu belongs to. + */ + SVPopupMenu(SVWindow sv) { + root = new JPopupMenu(); + svWindow = sv; + items = new HashMap<String, SVAbstractMenuItem>(); + } + + /** + * Add a new entry to the menubar. For these items, the server will poll the + * client to ask what to do. + * + * @param parent The menu we add our new entry to (should have been defined + * before). If the parent is "", we will add the entry to the root + * (top-level) + * @param name The caption of the new entry. + * @param id The Id of the new entry. If it is -1, the entry will be treated + * as a menu. + */ + public void add(String parent, String name, int id) { + // A duplicate entry - we just throw it away, since its already in. + if (items.get(name) != null) { return; } + // A new submenu at the top-level + if (parent.equals("")) { + JMenu jli = new JMenu(name); + SVAbstractMenuItem mli = new SVSubMenuItem(name, jli); + items.put(name, mli); + root.add(jli); + } + // A new sub-submenu + else if (id == -1) { + SVAbstractMenuItem jmi = items.get(parent); + JMenu jli = new JMenu(name); + SVAbstractMenuItem mli = new SVSubMenuItem(name, jli); + items.put(name, mli); + jmi.add(jli); + } + // A new child entry. Add to appropriate parent. + else { + SVAbstractMenuItem jmi = items.get(parent); + if (jmi == null) { + System.out.println("ERROR: Unknown parent " + parent); + System.exit(1); + } + SVAbstractMenuItem mli = new SVEmptyMenuItem(id, name); + mli.mi.addActionListener(this); + items.put(name, mli); + jmi.add(mli); + } + } + + /** + * Add a new entry to the menubar. In this case, we also know its value and + * possibly even have a description. For these items, the server will not poll + * the client to ask what to do, but just show an input dialog and send a + * message with the new value. + * + * @param parent The menu we add our new entry to (should have been defined + * before). If the parent is "", we will add the entry to the root + * (top-level) + * @param name The caption of the new entry. + * @param id The Id of the new entry. If it is -1, the entry will be treated + * as a menu. + * @param value The value of the new entry. + * @param desc The description of the new entry. + */ + public void add(String parent, String name, int id, String value, String desc) { + SVAbstractMenuItem jmi = items.get(parent); + SVMenuItem mli = new SVMenuItem(id, name, value, desc); + mli.mi.addActionListener(this); + items.put(name, mli); + if (jmi == null) { // add to root + root.add(mli.mi); + } else { // add to parent + jmi.add(mli); + } + } + + + + /** + * A click on one of the items in our menubar has occurred. Forward it + * to the item itself to let it decide what happens. + */ + public void actionPerformed(ActionEvent e) { + + // Get the corresponding menuitem + SVAbstractMenuItem svm = items.get(e.getActionCommand()); + + svm.performAction(svWindow, SVEventType.SVET_POPUP); + } + + /** + * Gets called by the SVEventHandler of the window to actually show the + * content of the popup menu. + */ + public void show(Component Invoker, int x, int y) { + root.show(Invoker, x, y); + } +} diff --git a/tesseract/java/com/google/scrollview/ui/SVSubMenuItem.java b/tesseract/java/com/google/scrollview/ui/SVSubMenuItem.java new file mode 100644 index 00000000..ebcf36cb --- /dev/null +++ b/tesseract/java/com/google/scrollview/ui/SVSubMenuItem.java @@ -0,0 +1,39 @@ +// 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; + +/** + * A MenuListItem is any sort of menu entry. This can either be within a popup + * menu or within a menubar. It can either be a submenu (only name and + * command-id) or a name with an associated value and possibly description. They + * can also have new entries added (if they are submenus). + * + * @author wanke@google.com + */ + +import javax.swing.JMenu; + +/** Constructs a new submenu which can hold other entries. */ +class SVSubMenuItem extends SVAbstractMenuItem { + public SVSubMenuItem(String name, JMenu jli) { + super(-1, name, jli); + } + /** Adds a child entry to the submenu. */ + @Override + public void add(SVAbstractMenuItem mli) { + mi.add(mli.mi); + } + /** Adds a child menu to the submenu (or root node). */ + @Override + public void add(JMenu jli) { + mi.add(jli); + } +} diff --git a/tesseract/java/com/google/scrollview/ui/SVWindow.java b/tesseract/java/com/google/scrollview/ui/SVWindow.java new file mode 100644 index 00000000..3b5e7cd8 --- /dev/null +++ b/tesseract/java/com/google/scrollview/ui/SVWindow.java @@ -0,0 +1,648 @@ +// 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; + + // 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(); + } +} |