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