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