• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2007 Google Inc. All Rights Reserved.
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License"); You may not
4 // use this file except in compliance with the License. You may obtain a copy of
5 // the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by
6 // applicable law or agreed to in writing, software distributed under the
7 // License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
8 // OF ANY KIND, either express or implied. See the License for the specific
9 // language governing permissions and limitations under the License.
10 
11 package com.google.scrollview.ui;
12 
13 import com.google.scrollview.ScrollView;
14 import com.google.scrollview.events.SVEvent;
15 import com.google.scrollview.events.SVEventHandler;
16 import com.google.scrollview.events.SVEventType;
17 import com.google.scrollview.ui.SVImageHandler;
18 import com.google.scrollview.ui.SVMenuBar;
19 import com.google.scrollview.ui.SVPopupMenu;
20 
21 import edu.umd.cs.piccolo.PCamera;
22 import edu.umd.cs.piccolo.PCanvas;
23 import edu.umd.cs.piccolo.PLayer;
24 
25 import edu.umd.cs.piccolo.nodes.PImage;
26 import edu.umd.cs.piccolo.nodes.PPath;
27 import edu.umd.cs.piccolo.nodes.PText;
28 import edu.umd.cs.piccolo.util.PPaintContext;
29 import edu.umd.cs.piccolox.swing.PScrollPane;
30 
31 import java.awt.BasicStroke;
32 import java.awt.BorderLayout;
33 import java.awt.Color;
34 import java.awt.Font;
35 import java.awt.GraphicsEnvironment;
36 import java.awt.geom.IllegalPathStateException;
37 import java.awt.Rectangle;
38 import java.awt.TextArea;
39 import java.util.regex.Matcher;
40 import java.util.regex.Pattern;
41 
42 import javax.swing.JFrame;
43 import javax.swing.JOptionPane;
44 import javax.swing.SwingUtilities;
45 import javax.swing.WindowConstants;
46 
47 /**
48  * The SVWindow is the top-level ui class. It should get instantiated whenever
49  * the user intends to create a new window. It contains helper functions to draw
50  * on the canvas, add new menu items, show modal dialogs etc.
51  *
52  * @author wanke@google.com
53  */
54 public class SVWindow extends JFrame {
55   /**
56    * Constants defining the maximum initial size of the window.
57    */
58   private static final int MAX_WINDOW_X = 1000;
59   private static final int MAX_WINDOW_Y = 800;
60 
61   /* Constant defining the (approx) height of the default message box*/
62   private static final int DEF_MESSAGEBOX_HEIGHT = 200;
63 
64   /** Constant defining the "speed" at which to zoom in and out. */
65   public static final double SCALING_FACTOR = 2;
66 
67   /** The top level layer we add our PNodes to (root node). */
68   PLayer layer;
69 
70   /** The current color of the pen. It is used to draw edges, text, etc. */
71   Color currentPenColor;
72 
73   /**
74    * The current color of the brush. It is used to draw the interior of
75    * primitives.
76    */
77   Color currentBrushColor;
78 
79   /** The system name of the current font we are using (e.g.
80    *  "Times New Roman"). */
81   Font currentFont;
82 
83   /** The stroke width to be used. */
84   // This really needs to be a fixed width stroke as the basic stroke is
85   // anti-aliased and gets too faint, but the piccolo fixed width stroke
86   // is too buggy and generates missing initial moveto in path definition
87   // errors with a IllegalPathStateException that cannot be caught because
88   // it is in the automatic repaint function. If we can fix the exceptions
89   // in piccolo, then we can use the following instead of BasicStroke:
90   //   import edu.umd.cs.piccolox.util.PFixedWidthStroke;
91   //   PFixedWidthStroke stroke = new PFixedWidthStroke(0.5f);
92   // Instead we use the BasicStroke and turn off anti-aliasing.
93   BasicStroke stroke = new BasicStroke(0.5f);
94 
95   /**
96    * A unique representation for the window, also known by the client. It is
97    * used when sending messages from server to client to identify him.
98    */
99   public int hash;
100 
101   /**
102    * The total number of created Windows. If this ever reaches 0 (apart from the
103    * beginning), quit the server.
104    */
105   public static int nrWindows = 0;
106 
107   /**
108    * The Canvas, MessageBox, EventHandler, Menubar and Popupmenu associated with
109    * this window.
110    */
111   private SVEventHandler svEventHandler = null;
112   private SVMenuBar svMenuBar = null;
113   private TextArea ta = null;
114   public SVPopupMenu svPuMenu = null;
115   public PCanvas canvas;
116   private int winSizeX;
117   private int winSizeY;
118 
119   /** Set the brush to an RGB color */
brush(int red, int green, int blue)120   public void brush(int red, int green, int blue) {
121     brush(red, green, blue, 255);
122   }
123 
124   /** Set the brush to an RGBA color */
brush(int red, int green, int blue, int alpha)125   public void brush(int red, int green, int blue, int alpha) {
126     // If alpha is zero, use a null brush to save rendering time.
127     if (alpha == 0) {
128       currentBrushColor = null;
129     } else {
130       currentBrushColor = new Color(red, green, blue, alpha);
131     }
132   }
133 
134   /** Erase all content from the window, but do not destroy it. */
clear()135   public void clear() {
136     layer.removeAllChildren();
137   }
138 
139   /**
140    * Start setting up a new image. The server will now expect image data until
141    * the image is complete.
142    *
143    * @param internalName The unique name of the new image
144    * @param width Image width
145    * @param height Image height
146    * @param bitsPerPixel The bit depth (currently supported: 1 (binary) and 32
147    *        (ARGB))
148    */
createImage(String internalName, int width, int height, int bitsPerPixel)149   public void createImage(String internalName, int width, int height,
150       int bitsPerPixel) {
151     SVImageHandler.createImage(internalName, width, height, bitsPerPixel);
152   }
153 
154   /**
155    * Start setting up a new polyline. The server will now expect
156    * polyline data until the polyline is complete.
157    *
158    * @param length number of coordinate pairs
159    */
createPolyline(int length)160   public void createPolyline(int length) {
161     ScrollView.polylineXCoords = new float[length];
162     ScrollView.polylineYCoords = new float[length];
163     ScrollView.polylineSize = length;
164     ScrollView.polylineScanned = 0;
165   }
166 
167   /**
168    * Draw the now complete polyline.
169    */
drawPolyline()170   public void drawPolyline() {
171     PPath pn = PPath.createPolyline(ScrollView.polylineXCoords,
172                                     ScrollView.polylineYCoords);
173     ScrollView.polylineSize = 0;
174     pn.setStrokePaint(currentPenColor);
175     pn.setPaint(null);  // Don't fill the polygon - this is just a polyline.
176     pn.setStroke(stroke);
177     layer.addChild(pn);
178   }
179 
180   /**
181    * Construct a new SVWindow and set it visible.
182    *
183    * @param name Title of the window.
184    * @param hash Unique internal representation. This has to be the same as
185    *        defined by the client, as they use this to refer to the windows.
186    * @param posX X position of where to draw the window (upper left).
187    * @param posY Y position of where to draw the window (upper left).
188    * @param sizeX The width of the window.
189    * @param sizeY The height of the window.
190    * @param canvasSizeX The canvas width of the window.
191    * @param canvasSizeY The canvas height of the window.
192    */
SVWindow(String name, int hash, int posX, int posY, int sizeX, int sizeY, int canvasSizeX, int canvasSizeY)193   public SVWindow(String name, int hash, int posX, int posY, int sizeX,
194                   int sizeY, int canvasSizeX, int canvasSizeY) {
195     super(name);
196 
197     // Provide defaults for sizes.
198     if (sizeX == 0) sizeX = canvasSizeX;
199     if (sizeY == 0) sizeY = canvasSizeY;
200     if (canvasSizeX == 0) canvasSizeX = sizeX;
201     if (canvasSizeY == 0) canvasSizeY = sizeY;
202 
203     // Initialize variables
204     nrWindows++;
205     this.hash = hash;
206     this.svEventHandler = new SVEventHandler(this);
207     this.currentPenColor = Color.BLACK;
208     this.currentBrushColor = Color.BLACK;
209     this.currentFont = new Font("Times New Roman", Font.PLAIN, 12);
210 
211     // Determine the initial size and zoom factor of the window.
212     // If the window is too big, rescale it and zoom out.
213     int shrinkfactor = 1;
214 
215     if (sizeX > MAX_WINDOW_X) {
216       shrinkfactor = (sizeX + MAX_WINDOW_X - 1) / MAX_WINDOW_X;
217     }
218     if (sizeY / shrinkfactor > MAX_WINDOW_Y) {
219       shrinkfactor = (sizeY + MAX_WINDOW_Y - 1) / MAX_WINDOW_Y;
220     }
221     winSizeX = sizeX / shrinkfactor;
222     winSizeY = sizeY / shrinkfactor;
223     double initialScalingfactor = 1.0 / shrinkfactor;
224     if (winSizeX > canvasSizeX || winSizeY > canvasSizeY) {
225       initialScalingfactor = Math.min(1.0 * winSizeX / canvasSizeX,
226                                       1.0 * winSizeY / canvasSizeY);
227     }
228 
229     // Setup the actual window (its size, camera, title, etc.)
230     if (canvas == null) {
231       canvas = new PCanvas();
232       getContentPane().add(canvas, BorderLayout.CENTER);
233     }
234 
235     layer = canvas.getLayer();
236     canvas.setBackground(Color.BLACK);
237 
238     // Disable anitaliasing to make the lines more visible.
239     canvas.setDefaultRenderQuality(PPaintContext.LOW_QUALITY_RENDERING);
240 
241     setLayout(new BorderLayout());
242 
243     setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
244 
245     validate();
246     canvas.requestFocus();
247 
248     // Manipulation of Piccolo's scene graph should be done from Swings
249     // event dispatch thread since Piccolo is not thread safe. This code calls
250     // initialize() from that thread once the PFrame is initialized, so you are
251     // safe to start working with Piccolo in the initialize() method.
252     SwingUtilities.invokeLater(new Runnable() {
253       public void run() {
254         repaint();
255       }
256     });
257 
258     setSize(winSizeX, winSizeY);
259     setLocation(posX, posY);
260     setTitle(name);
261 
262     // Add a Scrollpane to be able to scroll within the canvas
263     PScrollPane scrollPane = new PScrollPane(canvas);
264     getContentPane().add(scrollPane);
265     scrollPane.setWheelScrollingEnabled(false);
266     PCamera lc = canvas.getCamera();
267     lc.scaleViewAboutPoint(initialScalingfactor, 0, 0);
268 
269     // Disable the default event handlers and add our own.
270     addWindowListener(svEventHandler);
271     canvas.removeInputEventListener(canvas.getPanEventHandler());
272     canvas.removeInputEventListener(canvas.getZoomEventHandler());
273     canvas.addInputEventListener(svEventHandler);
274     canvas.addKeyListener(svEventHandler);
275 
276     // Make the window visible.
277     validate();
278     setVisible(true);
279 
280   }
281 
282   /**
283    * Convenience function to add a message box to the window which can be used
284    * to output debug information.
285    */
addMessageBox()286   public void addMessageBox() {
287     if (ta == null) {
288       ta = new TextArea();
289       ta.setEditable(false);
290       getContentPane().add(ta, BorderLayout.SOUTH);
291     }
292     // We need to make the window bigger to accomodate the message box.
293     winSizeY += DEF_MESSAGEBOX_HEIGHT;
294     setSize(winSizeX, winSizeY);
295   }
296 
297   /**
298    * Allows you to specify the thickness with which to draw lines, recantgles
299    * and ellipses.
300    * @param width The new thickness.
301    */
setStrokeWidth(float width)302   public void setStrokeWidth(float width) {
303     // If this worked we wouldn't need the antialiased rendering off.
304     // stroke = new PFixedWidthStroke(width);
305     stroke = new BasicStroke(width);
306   }
307 
308   /**
309    * Draw an ellipse at (x,y) with given width and height, using the
310    * current stroke, the current brush color to fill it and the
311    * current pen color for the outline.
312    */
drawEllipse(int x, int y, int width, int height)313   public void drawEllipse(int x, int y, int width, int height) {
314     PPath pn = PPath.createEllipse(x, y, width, height);
315     pn.setStrokePaint(currentPenColor);
316     pn.setStroke(stroke);
317     pn.setPaint(currentBrushColor);
318     layer.addChild(pn);
319   }
320 
321   /**
322    * Draw the image with the given name at (x,y). Any image loaded stays in
323    * memory, so if you intend to redraw an image, you do not have to use
324    * createImage again.
325    */
drawImage(String internalName, int x_pos, int y_pos)326   public void drawImage(String internalName, int x_pos, int y_pos) {
327     PImage img = SVImageHandler.getImage(internalName);
328     img.setX(x_pos);
329     img.setY(y_pos);
330     layer.addChild(img);
331   }
332 
333   /**
334    * Draw a line from (x1,y1) to (x2,y2) using the current pen color and stroke.
335    */
drawLine(int x1, int y1, int x2, int y2)336   public void drawLine(int x1, int y1, int x2, int y2) {
337     PPath pn = PPath.createLine(x1, y1, x2, y2);
338     pn.setStrokePaint(currentPenColor);
339     pn.setPaint(null);  // Null paint may render faster than the default.
340     pn.setStroke(stroke);
341     pn.moveTo(x1, y1);
342     pn.lineTo(x2, y2);
343     layer.addChild(pn);
344   }
345 
346   /**
347    * Draw a rectangle given the two points (x1,y1) and (x2,y2) using the current
348    * stroke, pen color for the border and the brush to fill the
349    * interior.
350    */
drawRectangle(int x1, int y1, int x2, int y2)351   public void drawRectangle(int x1, int y1, int x2, int y2) {
352 
353     if (x1 > x2) {
354       int t = x1;
355       x1 = x2;
356       x2 = t;
357     }
358     if (y1 > y2) {
359       int t = y1;
360       y1 = y2;
361       y2 = t;
362     }
363 
364     PPath pn = PPath.createRectangle(x1, y1, x2 - x1, y2 - y1);
365     pn.setStrokePaint(currentPenColor);
366     pn.setStroke(stroke);
367     pn.setPaint(currentBrushColor);
368     layer.addChild(pn);
369   }
370 
371   /**
372    * Draw some text at (x,y) using the current pen color and text attributes. If
373    * the current font does NOT support at least one character, it tries to find
374    * a font which is capable of displaying it and use that to render the text.
375    * Note: If the font says it can render a glyph, but in reality it turns out
376    * to be crap, there is nothing we can do about it.
377    */
drawText(int x, int y, String text)378   public void drawText(int x, int y, String text) {
379     int unreadableCharAt = -1;
380     char[] chars = text.toCharArray();
381     PText pt = new PText(text);
382     pt.setTextPaint(currentPenColor);
383     pt.setFont(currentFont);
384 
385     // Check to see if every character can be displayed by the current font.
386     for (int i = 0; i < chars.length; i++) {
387       if (!currentFont.canDisplay(chars[i])) {
388         // Set to the first not displayable character.
389         unreadableCharAt = i;
390         break;
391       }
392     }
393 
394     // Have to find some working font and use it for this text entry.
395     if (unreadableCharAt != -1) {
396       Font[] allfonts =
397           GraphicsEnvironment.getLocalGraphicsEnvironment().getAllFonts();
398       for (int j = 0; j < allfonts.length; j++) {
399         if (allfonts[j].canDisplay(chars[unreadableCharAt])) {
400           Font tempFont =
401               new Font(allfonts[j].getFontName(), currentFont.getStyle(),
402                   currentFont.getSize());
403           pt.setFont(tempFont);
404           break;
405         }
406       }
407     }
408 
409     pt.setX(x);
410     pt.setY(y);
411     layer.addChild(pt);
412   }
413 
414   /** Set the pen color to an RGB value */
pen(int red, int green, int blue)415   public void pen(int red, int green, int blue) {
416     pen(red, green, blue, 255);
417   }
418 
419   /** Set the pen color to an RGBA value */
pen(int red, int green, int blue, int alpha)420   public void pen(int red, int green, int blue, int alpha) {
421     currentPenColor = new Color(red, green, blue, alpha);
422   }
423 
424   /**
425    * Define how to display text. Note: underlined is not currently not supported
426    */
textAttributes(String font, int pixelSize, boolean bold, boolean italic, boolean underlined)427   public void textAttributes(String font, int pixelSize, boolean bold,
428       boolean italic, boolean underlined) {
429 
430     // For legacy reasons convert "Times" to "Times New Roman"
431     if (font.equals("Times")) {
432       font = "Times New Roman";
433     }
434 
435     int style = Font.PLAIN;
436     if (bold) {
437       style += Font.BOLD;
438     }
439     if (italic) {
440       style += Font.ITALIC;
441     }
442     currentFont = new Font(font, style, pixelSize);
443   }
444 
445   /**
446    * Zoom the window to the rectangle given the two points (x1,y1)
447    * and (x2,y2), which must be greater than (x1,y1).
448    */
zoomRectangle(int x1, int y1, int x2, int y2)449   public void zoomRectangle(int x1, int y1, int x2, int y2) {
450     if (x2 > x1 && y2 > y1) {
451       winSizeX = getWidth();
452       winSizeY = getHeight();
453       int width = x2 - x1;
454       int height = y2 - y1;
455       // Since piccolo doesn't do this well either, pad with a margin
456       // all the way around.
457       int wmargin = width / 2;
458       int hmargin = height / 2;
459       double scalefactor = Math.min(winSizeX / (2.0 * wmargin + width),
460                                     winSizeY / (2.0 * hmargin + height));
461       PCamera lc = canvas.getCamera();
462       lc.scaleView(scalefactor / lc.getViewScale());
463       lc.animateViewToPanToBounds(new Rectangle(x1 - hmargin, y1 - hmargin,
464                                                 2 * wmargin + width,
465                                                 2 * hmargin + height), 0);
466     }
467   }
468 
469   /**
470    * Flush buffers and update display.
471    *
472    * Only actually reacts if there are no more messages in the stack, to prevent
473    * the canvas from flickering.
474    */
update()475   public void update() {
476     // TODO(rays) fix bugs in piccolo or use something else.
477     // The repaint function generates many
478     // exceptions for no good reason. We catch and ignore as many as we
479     // can here, but most of them are generated by the system repaints
480     // caused by resizing/exposing parts of the window etc, and they
481     // generate unwanted stack traces that have to be piped to /dev/null
482     // (on linux).
483     try {
484       repaint();
485     } catch (NullPointerException e) {
486       // Do nothing so the output isn't full of stack traces.
487     } catch (IllegalPathStateException e) {
488       // Do nothing so the output isn't full of stack traces.
489     }
490   }
491 
492   /** Adds a checkbox entry to the menubar, c.f. SVMenubar.add(...) */
addMenuBarItem(String parent, String name, int id, boolean checked)493   public void addMenuBarItem(String parent, String name, int id,
494                              boolean checked) {
495     svMenuBar.add(parent, name, id, checked);
496   }
497 
498   /** Adds a submenu to the menubar, c.f. SVMenubar.add(...) */
addMenuBarItem(String parent, String name)499   public void addMenuBarItem(String parent, String name) {
500     addMenuBarItem(parent, name, -1);
501   }
502 
503   /** Adds a new entry to the menubar, c.f. SVMenubar.add(...) */
addMenuBarItem(String parent, String name, int id)504   public void addMenuBarItem(String parent, String name, int id) {
505     if (svMenuBar == null) {
506       svMenuBar = new SVMenuBar(this);
507 
508     }
509     svMenuBar.add(parent, name, id);
510   }
511 
512   /** Add a message to the message box. */
addMessage(String message)513   public void addMessage(String message) {
514     if (ta != null) {
515       ta.append(message + "\n");
516     } else {
517       System.out.println(message + "\n");
518     }
519   }
520 
521   /**
522    * This method converts a string which might contain hexadecimal values to a
523    * string which contains the respective unicode counterparts.
524    *
525    * For example, Hall0x0094chen returns Hall<o umlaut>chen
526    * encoded as utf8.
527    *
528    * @param input The original string, containing 0x values
529    * @return The converted string which has the replaced unicode symbols
530    */
convertIntegerStringToUnicodeString(String input)531   private static String convertIntegerStringToUnicodeString(String input) {
532     StringBuffer sb = new StringBuffer(input);
533     Pattern numbers = Pattern.compile("0x[0-9a-fA-F]{4}");
534     Matcher matcher = numbers.matcher(sb);
535 
536     while (matcher.find()) {
537       // Find the next match which resembles a hexadecimal value and convert it
538       // to
539       // its char value
540       char a = (char) (Integer.decode(matcher.group()).intValue());
541 
542       // Replace the original with the new character
543       sb.replace(matcher.start(), matcher.end(), String.valueOf(a));
544 
545       // Start again, since our positions have switched
546       matcher.reset();
547     }
548     return sb.toString();
549   }
550 
551   /**
552    * Show a modal input dialog. The answer by the dialog is then send to the
553    * client, together with the associated menu id, as SVET_POPUP
554    *
555    * @param msg The text that is displayed in the dialog.
556    * @param def The default value of the dialog.
557    * @param id The associated commandId
558    * @param evtype The event this is associated with (usually SVET_MENU
559    * or SVET_POPUP)
560    */
showInputDialog(String msg, String def, int id, SVEventType evtype)561   public void showInputDialog(String msg, String def, int id,
562                               SVEventType evtype) {
563     svEventHandler.timer.stop();
564     String tmp =
565         (String) JOptionPane.showInputDialog(this, msg, "",
566             JOptionPane.QUESTION_MESSAGE, null, null, def);
567 
568     if (tmp != null) {
569       tmp = convertIntegerStringToUnicodeString(tmp);
570       SVEvent res = new SVEvent(evtype, this, id, tmp);
571       ScrollView.addMessage(res);
572     }
573     svEventHandler.timer.restart();
574   }
575 
576 
577   /**
578    * Shows a modal input dialog to the user. The return value is automatically
579    * sent to the client as SVET_INPUT event (with command id -1).
580    *
581    * @param msg The text of the dialog.
582    */
showInputDialog(String msg)583   public void showInputDialog(String msg) {
584     showInputDialog(msg, null, -1, SVEventType.SVET_INPUT);
585   }
586 
587   /**
588    * Shows a dialog presenting "Yes" and "No" as answers and returns either a
589    * "y" or "n" to the client.
590    *
591    * @param msg The text that is displayed in the dialog.
592    */
showYesNoDialog(String msg)593   public void showYesNoDialog(String msg) {
594     // res returns 0 on yes, 1 on no. Seems to be a bit counterintuitive
595     int res =
596         JOptionPane.showOptionDialog(this, msg, "", JOptionPane.YES_NO_OPTION,
597             JOptionPane.QUESTION_MESSAGE, null, null, null);
598     SVEvent e = null;
599 
600     if (res == 0) {
601       e = new SVEvent(SVEventType.SVET_INPUT, this, 0, 0, 0, 0, "y");
602     } else if (res == 1) {
603       e = new SVEvent(SVEventType.SVET_INPUT, this, 0, 0, 0, 0, "n");
604     }
605     ScrollView.addMessage(e);
606   }
607 
608   /** Adds a submenu to the popup menu, c.f. SVPopupMenu.add(...) */
addPopupMenuItem(String parent, String name)609   public void addPopupMenuItem(String parent, String name) {
610     if (svPuMenu == null) {
611       svPuMenu = new SVPopupMenu(this);
612     }
613     svPuMenu.add(parent, name, -1);
614   }
615 
616   /** Adds a new menu entry to the popup menu, c.f. SVPopupMenu.add(...) */
addPopupMenuItem(String parent, String name, int cmdEvent, String value, String desc)617   public void addPopupMenuItem(String parent, String name, int cmdEvent,
618       String value, String desc) {
619     if (svPuMenu == null) {
620       svPuMenu = new SVPopupMenu(this);
621     }
622     svPuMenu.add(parent, name, cmdEvent, value, desc);
623   }
624 
625   /** Destroys a window. */
destroy()626   public void destroy() {
627     ScrollView.addMessage(new SVEvent(SVEventType.SVET_DESTROY, this, 0,
628         "SVET_DESTROY"));
629     setVisible(false);
630     // dispose();
631   }
632 
633   /**
634    * Open an image from a given file location and store it in memory. Pro:
635    * Faster than createImage. Con: Works only on the local file system.
636    *
637    * @param filename The path to the image.
638    */
openImage(String filename)639   public void openImage(String filename) {
640     SVImageHandler.openImage(filename);
641   }
642 
643 }
644