/* * Copyright (c) 2007 BUSINESS OBJECTS SOFTWARE LIMITED * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * * Neither the name of Business Objects nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ /* * TableTopGemPainter.java * Creation date: (03/12/02 11:11:00 AM) * By: Edward Lam */ package org.openquark.gems.client; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Font; import java.awt.FontMetrics; import java.awt.GradientPaint; import java.awt.Graphics2D; import java.awt.Image; import java.awt.Paint; import java.awt.Point; import java.awt.Polygon; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.Shape; import java.awt.Stroke; import java.awt.TexturePaint; import java.awt.geom.Area; import java.awt.geom.Rectangle2D; import java.awt.geom.RoundRectangle2D; import java.awt.image.BufferedImage; import java.awt.image.ConvolveOp; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.WeakHashMap; import javax.swing.ImageIcon; import org.openquark.gems.client.DisplayedGem.DisplayedPartOutput; import org.openquark.gems.client.IntellicutManager.IntellicutMode; import org.openquark.gems.client.internal.effects.GaussianKernel; import org.openquark.gems.client.internal.effects.SimpleEffects; import org.openquark.gems.client.internal.effects.Voronoi; import org.openquark.util.Pair; /** * A GemPainter takes a gem and paints it into the tabletop * @author Edward Lam */ class TableTopGemPainter implements DisplayedGemLocationListener, DisplayedGemSizeListener { /* * There are a number of potential performance improvements to this class: * * Coalescing of line drawing * - calling Graphics.drawLine() many times in succession is much faster than drawLine() calls interspersed with other Graphics draw calls. * * Calling specialized Graphics draw() methods. * - a number of Graphics.drawLine() calls is much faster than unspecialized Graphics2D.draw(shape). * - drawOval(), drawRoundRect(), fillXXX(), are faster than the unspecialized draw() and fill() methods. * - One drawback is that the specialized draw() methods take int arguments, however in some cases the coordinates we want * to specify cannot be expressed in these terms (eg. x = 1.5, y = 0.3). * * Caching of image data * - more caching could be used. * - for instance, the halo image for a rectangle (round or not) could be broken into corner sections and edge sections. * The image could be constructed by adding the corner sections to appropriately scaled edge sections. * Probably the same could be done for triangles, rotating the edge section data, if we could figure out how to do the corners. * * Creation of new Graphics objects using Graphics.create() (then dispose()). * - Can be used instead of saving and resetting Graphics object properties (such as clip or color). * - This is heavily optimized, as it is used extensively within AWT/Swing. * * We might also be able to save some work painting triangles, if we check for intersection using Graphics2D.hit(). * */ /** The tabletop in which to paint. */ private final TableTop tableTop; /** Crazy paving cache **/ private final transient Map<DisplayedGem, Voronoi> crazyPavingCache = new WeakHashMap<DisplayedGem, Voronoi>(); /** Cached gem tape label */ private static final transient Map<DisplayedGem, BufferedImage> tapeLabelCache = new WeakHashMap<DisplayedGem, BufferedImage>(); /** Cached gem field name tape label, used for RecordCreationGems */ private static final transient Map<DisplayedGem, Map<String, BufferedImage>> fieldNameTagLabelCache = new WeakHashMap<DisplayedGem, Map<String, BufferedImage>>(); /** Run halo cache */ private final transient Map<DisplayedGem, BufferedImage> runHaloCache = new WeakHashMap<DisplayedGem, BufferedImage>(); /** Select halo cache */ private final transient Map<DisplayedGem, Pair<Color, BufferedImage>> selectHaloCache = new WeakHashMap<DisplayedGem, Pair<Color, BufferedImage>>(); /** The image to use to indicate that a code gem's code editor is open but not selected. */ private static final Image codeEditorOpenImage; /** The image to use to indicate that a code gem's code editor is open and selected. */ private static final Image codeEditorOpenAndSelectedImage; /** The crack color for non-photolook tabletops. */ private static final Color PLAIN_CRACK_COLOUR = new Color(200, 200, 200); /** The text outline colour for target gem text. */ private static final Color TARGET_TEXT_OUTLINE_COLOUR = new Color(5, 5, 5, 100); // Initialize the open code editor image. static { codeEditorOpenImage = new ImageIcon(TableTopGemPainter.class.getResource("/Resources/codeEditorOpen.gif")).getImage(); codeEditorOpenAndSelectedImage = new ImageIcon(TableTopGemPainter.class.getResource("/Resources/selectedCodeEditorOpen.gif")).getImage(); } /** * Constructor for a tabletop gem painter. */ TableTopGemPainter(TableTop tableTop) { this.tableTop = tableTop; } /** * The paint method for a Gem. * @param g2d Graphics2D the graphics context * @param displayedGem DisplayedGem the gem to paint */ public void paintGem(DisplayedGem displayedGem, Graphics2D g2d) { Color prevColour = g2d.getColor(); // Draw the halo image if necessary. paintHalo(displayedGem, g2d); // Check for an intersection with the paint region if (displayedGem.getBounds().intersects(g2d.getClipBounds())) { // Paint the body. paintBody(displayedGem, g2d); // Paint the brokenness indicators as necessary paintCrazyPaving(displayedGem, g2d); // Paint the focus outline paintFocus(displayedGem, g2d); // Draw the output line and bind point if output connected (colour based on output type) paintOutputPart(displayedGem, g2d); // Draw the input lines and bind points int scArity = displayedGem.getNDisplayedArguments(); for (int i = 0; i < scArity; i++) { paintInputPart(displayedGem, i, g2d); } displayedGem.getDisplayedGemShape().getInnerComponentInfo().paint(tableTop, g2d); } g2d.setColor(prevColour); } /** * Paint the focus outline (if any) for the given displayed gem. * @param displayedGem * @param g2d */ private void paintFocus(DisplayedGem displayedGem, Graphics2D g2d) { if (tableTop.getFocusedDisplayedGem() == displayedGem) { BasicStroke oldStroke = (BasicStroke)g2d.getStroke(); BasicStroke highlightStroke; if (displayedGem.getDisplayedGemShape() instanceof DisplayedGemShape.Triangular && tableTop.getTableTopPanel().isPhotoLook()) { highlightStroke = new BasicStroke(oldStroke.getLineWidth() * 4, BasicStroke.CAP_ROUND, oldStroke.getLineJoin(), oldStroke.getMiterLimit()); g2d.setColor(getTableTopBaseColour()); } else { highlightStroke = new BasicStroke(oldStroke.getLineWidth(), BasicStroke.CAP_ROUND, oldStroke.getLineJoin(), oldStroke.getMiterLimit(), new float[]{1, 12}, 0); g2d.setColor(Color.yellow); } g2d.setStroke(highlightStroke); g2d.draw(displayedGem.getBodyShape()); // Restore the original Stroke object g2d.setStroke(oldStroke); } } /** * Paint the halo for the given displayed gem, as necessary. * @param displayedGem the displayed gem in question. * @param g2d the graphics context into which to draw. */ private void paintHalo(DisplayedGem displayedGem, Graphics2D g2d) { // If this Gem is selected or running, check for an intersection with the halo Rectangle haloRect = getHaloBounds(displayedGem); if (haloRect.intersects(g2d.getClipBounds())) { // Grab the appropriate halo image (if any) BufferedImage haloImage = null; if (tableTop.isRunning(displayedGem)) { haloImage = getRunHaloImage(displayedGem); } else if (tableTop.isSelected(displayedGem)){ haloImage = getSelectHaloImage(displayedGem, getGemHaloColour(displayedGem)); } // If there is a halo image, draw this buffer with the correct offset if (haloImage != null) { g2d.drawImage(haloImage, null, haloRect.x, haloRect.y); } } } /** * Determine the Gem's halo bounds. * This is bounds of the selection halo which appears when the gem is selected. * @param displayedGem * @return Rectangle the bounds of the body section */ private Rectangle getHaloBounds(DisplayedGem displayedGem){ Rectangle haloRect = new Rectangle(displayedGem.getDisplayedBodyPart().getBounds()); int haloPlusBlur = DisplayConstants.HALO_SIZE + DisplayConstants.HALO_BLUR_SIZE; haloRect.x -= haloPlusBlur; haloRect.y -= haloPlusBlur; haloRect.width += haloPlusBlur * 2; haloRect.height += haloPlusBlur * 2; return haloRect; } /** * Return a Gem's halo, in the given highlight colour. * @param displayedGem * @param gemHighlightColour Color the colour of the halo * @return BufferedImage */ private BufferedImage getHaloImage(DisplayedGem displayedGem, Color gemHighlightColour) { Rectangle haloRect = getHaloBounds(displayedGem); // Build blurred shape in buffered image BufferedImage haloImage = new BufferedImage(haloRect.width, haloRect.height, BufferedImage.TYPE_INT_ARGB); Graphics2D bg = haloImage.createGraphics(); bg.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); bg.setPaint(gemHighlightColour); Shape blurShape; // Define and draw shape if (displayedGem.getDisplayedGemShape() instanceof DisplayedGemShape.Triangular) { // triangular Polygon blurPoly = new Polygon(); int LHSPerpendicular = (int) Math.sqrt(DisplayConstants.HALO_SIZE * DisplayConstants.HALO_SIZE - DisplayConstants.HALO_BEVEL_SIZE * DisplayConstants.HALO_BEVEL_SIZE); Point blur1 = new Point(DisplayConstants.HALO_SIZE + DisplayConstants.HALO_BLUR_SIZE - DisplayConstants.HALO_BEVEL_SIZE, DisplayConstants.HALO_SIZE + DisplayConstants.HALO_BLUR_SIZE - LHSPerpendicular); Point blur2 = new Point(haloRect.width, haloRect.height / 2); Point blur3 = new Point(DisplayConstants.HALO_SIZE + DisplayConstants.HALO_BLUR_SIZE - DisplayConstants.HALO_BEVEL_SIZE, haloRect.height - (DisplayConstants.HALO_SIZE + DisplayConstants.HALO_BLUR_SIZE - LHSPerpendicular)); blurPoly.addPoint(blur1.x, blur1.y); blurPoly.addPoint(blur2.x, blur2.y); blurPoly.addPoint(blur3.x, blur3.y); blurShape = blurPoly; } else { // rectangular double haloX = DisplayConstants.HALO_SIZE + DisplayConstants.HALO_BLUR_SIZE - DisplayConstants.HALO_BEVEL_SIZE; double haloY = DisplayConstants.HALO_SIZE + DisplayConstants.HALO_BLUR_SIZE - DisplayConstants.HALO_BEVEL_SIZE; double haloWidth = haloRect.width - 2 * haloX; double haloHeight = haloRect.height - 2 * haloY; if (displayedGem.getDisplayedGemShape() instanceof DisplayedGemShape.Oval) { double arcSize = haloRect.getHeight()/2.0; blurShape = new RoundRectangle2D.Double(haloX, haloY, haloWidth, haloHeight, arcSize, arcSize); } else if (displayedGem.getDisplayedGemShape() instanceof DisplayedGemShape.Rectangular) { blurShape = new Rectangle2D.Double(haloX, haloY, haloWidth, haloHeight); } else { throw new IllegalArgumentException("Don't know how to paint this gem shape: " + displayedGem.getDisplayedGemShape().getClass()); } } bg.fill(blurShape); bg.dispose(); // Blur shape ConvolveOp blur = new ConvolveOp(new GaussianKernel(DisplayConstants.HALO_BLUR_SIZE)); haloImage = blur.filter(haloImage, null); return haloImage; } /** * Return a Gem's run halo * @param displayedGem * @return BufferedImage the run halo */ private BufferedImage getRunHaloImage(DisplayedGem displayedGem) { BufferedImage cachedRunHaloImage = runHaloCache.get(displayedGem); if (cachedRunHaloImage == null) { cachedRunHaloImage = getHaloImage(displayedGem, DisplayConstants.RUN_HALO_COLOUR); runHaloCache.put(displayedGem, cachedRunHaloImage); } return cachedRunHaloImage; } /** * Return a Gem's selection halo * @param displayedGem * @param haloColour * @return BufferedImage the selection halo */ private BufferedImage getSelectHaloImage(DisplayedGem displayedGem, Color haloColour) { BufferedImage cachedSelectHaloImage; Pair<Color, BufferedImage> cachedColourImagePair = selectHaloCache.get(displayedGem); if (cachedColourImagePair == null || !cachedColourImagePair.fst().equals(haloColour)) { cachedSelectHaloImage = getHaloImage(displayedGem, haloColour); selectHaloCache.put(displayedGem, new Pair<Color, BufferedImage>(haloColour, cachedSelectHaloImage)); } else { cachedSelectHaloImage = cachedColourImagePair.snd(); } return cachedSelectHaloImage; } /** * Paint broken cracks to show that a gem is broken. * @param displayedGem * @param g2d */ private void paintCrazyPaving(DisplayedGem displayedGem, Graphics2D g2d) { // If this Gem is 'broken' draw the 'crazy paving' if (displayedGem.getGem().isBroken()) { Voronoi cachedCrazyPaving = crazyPavingCache.get(displayedGem); if (cachedCrazyPaving == null) { // Generate an appropriate crazy paving effect cachedCrazyPaving = Voronoi.makeRandomAreaVoronoi(displayedGem.getDisplayedBodyPart().getBounds(), DisplayConstants.CRACK_POINTS_PER_QTR); crazyPavingCache.put(displayedGem, cachedCrazyPaving); } // Clip to triangle region Shape oldClip = g2d.getClip(); g2d.setClip(displayedGem.getBodyShape()); // Draw the crazy paving! g2d.setColor(getCrackColour(displayedGem)); cachedCrazyPaving.show(g2d, true, false); // Restore clipping g2d.setClip(oldClip); } } /** * Get an InnerComponentInfo representing the painting of a tape label. * @param displayedGem the displayed gem into which to paint. * @return the corresponding InnerComponentInfo. */ static DisplayedGemShape.InnerComponentInfo getNameTapeLabelInfo(final DisplayedGem displayedGem) { return new DisplayedGemShape.InnerComponentInfo() { Font font = GemCutterPaintHelper.getTitleFont(); public Rectangle getBounds() { Graphics2D g2d = (Graphics2D)DisplayedGemShape.getGraphics(); return paintNameTapeLabel(displayedGem, font, g2d); } public List<Rectangle> getInputNameLabelBounds(){ if(! (displayedGem.getGem() instanceof RecordCreationGem)){ return null; } final Graphics2D g2d = (Graphics2D)DisplayedGemShape.getGraphics(); return paintFieldNameTag(displayedGem, font, g2d); } public void paint(TableTop tableTop, Graphics2D g2d) { Gem gem = displayedGem.getGem(); // Draw the field names instead if the gem is a recordCreationGem if (gem instanceof RecordCreationGem){ paintFieldNameTag(displayedGem, font, g2d); } else { Rectangle labelBounds = paintNameTapeLabel(displayedGem, font, g2d); // If the gem is a code gem and its code editor is open then draw the appropriate indication of this. if (gem instanceof CodeGem && tableTop.isCodeEditorVisible((CodeGem)gem)) { // If one of the editor's children has focus then we want to use the selected open // editor image to indicate this editor is "activated" Image editorImage = null; if (tableTop.getCodeGemEditor((CodeGem)gem).isFocused()) { editorImage = codeEditorOpenAndSelectedImage; } else { editorImage = codeEditorOpenImage; } // Draw the image just below the left side of the name text g2d.drawImage(editorImage, labelBounds.x, labelBounds.y + labelBounds.height - 1, null); } } } }; } /** * Paint a label with the gem's display text, in the middle of the gem. * @param displayedGem * @param font * @param g2d * @return the string bounds of the tape label. */ private static Rectangle paintNameTapeLabel(DisplayedGem displayedGem, Font font, Graphics2D g2d) { boolean isLetGem = displayedGem.getGem() instanceof CollectorGem; // Set the text font g2d.setFont(font); FontMetrics fm = g2d.getFontMetrics(); // Get the gem text String gemText = displayedGem.getDisplayText(); // Get the label (create as necessary). BufferedImage labelImage = tapeLabelCache.get(displayedGem); if (labelImage == null) { // Create label image int tearMarginMedian = (DisplayConstants.INPUT_OUTPUT_LABEL_MARGIN - 1) / 2; Rectangle fullLabelSize = fm.getStringBounds(gemText, g2d).getBounds(); labelImage = SimpleEffects.makeRippedTapeStripLabel(fullLabelSize.getSize(), tearMarginMedian, DisplayConstants.LABEL_COLOUR); tapeLabelCache.put(displayedGem, labelImage); } // Clip to triangle region, this _could_ be the inner triangle for the label... Shape oldClip = g2d.getClip(); g2d.clip(displayedGem.getBodyShape()); // Get the bounds of the body. Rectangle bodyBounds = displayedGem.getDisplayedBodyPart().getBounds(); // Draw the label image. int labelX = isLetGem ? bodyBounds.x + (bodyBounds.width - labelImage.getWidth()) / 2 : bodyBounds.x + DisplayConstants.BEVEL_WIDTH_X + 1; int labelY = bodyBounds.y + ((bodyBounds.height - labelImage.getHeight()) / 2); g2d.drawImage(labelImage, null, labelX, labelY); // Set the text colour g2d.setColor(Color.black); // Determine the baseline for vertically centered text float baselineToCentre = (float)((fm.getAscent() - fm.getDescent()) / 2.0); float centredBaseline = (float)bodyBounds.getCenterY() + baselineToCentre; // Draw the title float labelMargin = isLetGem ? DisplayConstants.LET_LABEL_MARGIN : (DisplayConstants.BEVEL_WIDTH_X + DisplayConstants.INPUT_OUTPUT_LABEL_MARGIN + 1); g2d.drawString(gemText, bodyBounds.x + labelMargin, centredBaseline - 1); // Restore clip g2d.setClip(oldClip); Rectangle bounds = fm.getStringBounds(gemText, g2d).getBounds(); return new Rectangle(labelX, labelY, bounds.width, bounds.height); } /** * Paint labels for the RecordCreationGem's fields, beside its corresponding input. * @param dGem * @param font * @param g2d */ private static List<Rectangle> paintFieldNameTag(DisplayedGem dGem, Font font, Graphics2D g2d){ // the rectangle which will be the union of all rectangles List<Rectangle> nameTagBounds = new ArrayList<Rectangle>(); // Set the text font & color g2d.setFont(font); g2d.setColor(Color.black); FontMetrics fm = g2d.getFontMetrics(); // Determine the baseline for vertically centered text float baselineToCentre = (float)((fm.getAscent() - fm.getDescent()) / 2.0); // Get the fieldName to labelImage map for the gem Map<String, BufferedImage> textToImageCache = fieldNameTagLabelCache.get(dGem); if (textToImageCache == null){ textToImageCache = new HashMap<String, BufferedImage>(); } // used for adjusting the X point for the label int inBoundsWidth = dGem.getDisplayedGemShape().getInBounds().width; // Clip to triangle region, this _could_ be the inner triangle for the label... Shape oldClip = g2d.getClip(); g2d.clip(dGem.getBodyShape()); // Draw the label for each field name List<String> names = ((RecordCreationGem)dGem.getGem()).getCopyOfFieldsList(); for (int i = 0; i < names.size(); i++) { String fieldNameText = names.get(i); BufferedImage labelImage = textToImageCache.get(fieldNameText); // Create the label if doesn't already exist in the cache if (labelImage == null) { int tearMarginMedian = (DisplayConstants.INPUT_OUTPUT_LABEL_MARGIN - 1) / 2; Rectangle fullLabelSize = fm.getStringBounds(fieldNameText, g2d).getBounds(); labelImage = SimpleEffects.makeRippedTapeStripLabel(fullLabelSize.getSize(), tearMarginMedian, DisplayConstants.LABEL_COLOUR); textToImageCache.put(fieldNameText, labelImage); } // Draw the label image. Point connectPoint = dGem.getDisplayedGemShape().getInputConnectPoint(i); int labelX = connectPoint.x + inBoundsWidth + DisplayConstants.BEVEL_WIDTH_X ; int labelY = connectPoint.y - labelImage.getHeight()/2; g2d.drawImage(labelImage, null, labelX, labelY); // draw the title, location is adjusted so the string is in the center of the label image g2d.drawString(fieldNameText, (float)labelX + DisplayConstants.INPUT_OUTPUT_LABEL_MARGIN, (float)connectPoint.y + baselineToCentre ); // The rectangle of the whole label Rectangle nameTagBound = new Rectangle(labelX, labelY, labelImage.getWidth(), labelImage.getHeight()); nameTagBounds.add(nameTagBound); } // Place the map for this gem in the cache fieldNameTagLabelCache.put(dGem, textToImageCache); // Restore clip g2d.setClip(oldClip); return nameTagBounds; } /** * Get an InnerComponentInfo representing the painting of a name in bold text. * @param displayedGem the displayed gem into which to paint. * @return the corresponding InnerComponentInfo. */ static DisplayedGemShape.InnerComponentInfo getBoldNameInfo(final DisplayedGem displayedGem) { return new DisplayedGemShape.InnerComponentInfo() { public Rectangle getBounds() { Graphics2D g2d = (Graphics2D)DisplayedGemShape.getGraphics(); return paintBoldName(displayedGem, Color.BLACK, null, g2d); } public void paint(TableTop tableTop, Graphics2D g2d) { Color textColour = getTextColour(displayedGem); Color outlineColour = tableTop.getTargetDisplayedCollector() == displayedGem ? TARGET_TEXT_OUTLINE_COLOUR : null; paintBoldName(displayedGem, textColour, outlineColour, g2d); } public List<Rectangle> getInputNameLabelBounds() { return null; } }; } /** * Paint the gem's display text in bold, in the middle of the gem. * @param displayedGem the gem for which the name will be painted. * @param textColour the colour of the text. * @param outlineColour the colour of the outline, if non-null. * @param g2d */ private static Rectangle paintBoldName(DisplayedGem displayedGem, Color textColour, Color outlineColour, Graphics2D g2d) { String displayText = displayedGem.getDisplayText(); Rectangle bodyRect = displayedGem.getDisplayedBodyPart().getBounds(); // Determine the baseline for vertically centred text g2d.setFont(GemCutterPaintHelper.getBoldFont()); FontMetrics fm = g2d.getFontMetrics(); float baselineToCentre = (float)((fm.getAscent() - fm.getDescent()) / 2.0); float centredBaseline = (float)(bodyRect.getCenterY() + baselineToCentre); float textX = bodyRect.x + DisplayConstants.LET_LABEL_MARGIN; float textY = centredBaseline - 1; // seems to be off by 1 for some reason. if (outlineColour != null) { g2d.setColor(outlineColour); g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); float outlineX = textX - DisplayConstants.OUTLINE_SIZE; float outlineY = textY - DisplayConstants.OUTLINE_SIZE; for (int x = 0, xmax = 2 * DisplayConstants.OUTLINE_SIZE; x <= xmax; x++) { for (int y = 0, ymax = 2 * DisplayConstants.OUTLINE_SIZE; y <= ymax; y++) { g2d.drawString(displayText, outlineX + x, outlineY + y); } } g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_OFF); } // Draw the title g2d.setColor(textColour); g2d.drawString(displayText, textX, textY); Rectangle bounds = fm.getStringBounds(displayText, g2d).getBounds(); return new Rectangle((int)textX, (int)textY, bounds.width, bounds.height); } /** * Get a new BufferedImage with concentric rings alternating between two given colours. * Note: due to a bug in GradientPaint, if paint1 is a GradientPaint, and paint2 is an instance of Color, any transparency * in paint1 will show paint2. * @param width the width of the image. * @param height the height of the image. * @param paint1 ring paint. This will be the paint for the centre ring. * @param paint2 ring paint. * @return the resulting image. */ private static BufferedImage getTargetImage(int width, int height, Paint paint1, Paint paint2) { BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_4BYTE_ABGR); Graphics2D g2d = image.createGraphics(); // Fill with paint2. g2d.setPaint(paint2); g2d.fillRect(0, 0, width, height); int nRings = (((width+1) / 2) / DisplayConstants.TARGET_RING_WIDTH) + 1; Point centerPoint = new Point(width/2, height/2); g2d.setPaint(paint1); // Draw circles with paint1. for (int i = nRings - 1; i > -1; i--) { if (i % 2 == 0) { drawCircle(g2d, (i+1) * DisplayConstants.TARGET_RING_WIDTH, DisplayConstants.TARGET_RING_WIDTH, centerPoint); } } return image; } /** * Draw a circle into the given graphics object. * @param g2d the graphics object into which to draw. * @param radius the radius of the circle. * @param lineWidth the width of the line used to draw the circle. * @param centerPoint the centre of the circle. */ private static void drawCircle(Graphics2D g2d, int radius, int lineWidth, Point centerPoint) { int xcenter = centerPoint.x; int ycenter = centerPoint.y; Stroke oldStroke = g2d.getStroke(); g2d.setStroke(new BasicStroke(lineWidth)); // save rendering hints, turn on anti-aliasing so that round edges look round. Object oldAntiAliasRenderingHint = g2d.getRenderingHint(RenderingHints.KEY_ANTIALIASING); g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2d.drawOval(xcenter-radius, ycenter-radius, 2*radius, 2*radius);//fill circles g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, oldAntiAliasRenderingHint); g2d.setStroke(oldStroke); } /** * Paint the body of the displayed gem. * @param displayedGem * @param g2d */ private void paintBody(DisplayedGem displayedGem, Graphics2D g2d) { DisplayedGemShape displayedGemShape = displayedGem.getDisplayedGemShape(); if (displayedGemShape instanceof DisplayedGemShape.Triangular) { paintBodyTriangle(displayedGem, g2d); } else if (displayedGemShape instanceof DisplayedGemShape.Oval) { Color gemColour = getBaseGemColour(displayedGem); Paint paint; if (tableTop.getTargetDisplayedCollector() == displayedGem) { Rectangle bodyRect = displayedGem.getDisplayedBodyPart().getBounds(); Paint redGradientPaint = new GradientPaint(0, 0, Color.RED, 25, 32, getTableTopBaseColour(), true); Paint gemColourGradientPaint = new GradientPaint(0, 0, new Color(5, 5, 5, 200), 25, 32, new Color(5, 5, 5, 75), true); BufferedImage image = getTargetImage(bodyRect.width, bodyRect.height, gemColourGradientPaint, redGradientPaint); paint = new TexturePaint(image, bodyRect); } else { paint = gemColour; } paintBodyOval(displayedGem, paint, g2d); } else if (displayedGemShape instanceof DisplayedGemShape.Rectangular){ paintBodyRectangle(displayedGem, g2d); } else { throw new IllegalArgumentException("Don't know how to paint this gem: " + displayedGem.getClass()); } } /** * Paint the body of the displayed gem as an oval shape. * @param displayedGem * @param g2d */ private void paintBodyOval(DisplayedGem displayedGem, Paint paint, Graphics2D g2d) { // The main Gem shape Shape bodyShape = displayedGem.getBodyShape(); // save rendering hints, turn on anti-aliasing so that round edges look round. Object oldAntiAliasRenderingHint = g2d.getRenderingHint(RenderingHints.KEY_ANTIALIASING); g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // Fill the Gem g2d.setPaint(paint); g2d.fill(bodyShape); // Draw the solid outline of the Gem g2d.setColor(Color.black); g2d.draw(bodyShape); // restore old rendering hint g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, oldAntiAliasRenderingHint); } /** * Paint the body of the displayed gem as an triangle (trillion?) shape. * @param displayedGem * @param g2d */ private void paintBodyTriangle(DisplayedGem displayedGem, Graphics2D g2d) { Color gemColour = getGemColour(displayedGem); Color baseColour = getTableTopBaseColour(); // The main Gem triangle Polygon gemTri = (Polygon)displayedGem.getBodyShape(); int[] triAxs = gemTri.xpoints; int[] triAys = gemTri.ypoints; int width = triAxs[1] - triAxs[2]; int height = triAys[2] - triAys[1]; // The 'facets' triangle Polygon facetTri = new Polygon(); // Calculate the distance between the right outer vertex, and the right 'facet' vertex int rightVertexDifference = (int) (Math.sqrt( width * width + height * height ) * (DisplayConstants.DIAGONAL_BEVEL_SIZE) / height); // Find the Left Hand Side Perpendicular difference int LHSPerpendicular = (DisplayConstants.BEVEL_WIDTH_X + rightVertexDifference) * height / width; // Find the points for the inner triangle Point triB1 = new Point(triAxs[0] + DisplayConstants.BEVEL_WIDTH_X, triAys[0] + LHSPerpendicular); Point triB2 = new Point(triAxs[1] - rightVertexDifference, triAys[1]); Point triB3 = new Point(triAxs[2] + DisplayConstants.BEVEL_WIDTH_X, triAys[2] - LHSPerpendicular); facetTri.addPoint(triB1.x, triB1.y); facetTri.addPoint(triB2.x, triB2.y); facetTri.addPoint(triB3.x, triB3.y); // Fill the Gem Shape oldClip = g2d.getClip(); Area clipArea = new Area(oldClip); clipArea.subtract(new Area(facetTri)); g2d.setClip(clipArea); Paint gradient = new GradientPaint(0, 0, gemColour, 15, 25, baseColour, true); g2d.setPaint(gradient); g2d.fill(gemTri); g2d.setClip(oldClip); // Draw and fill the facet triangle Color highlightColour = getGemHighlightColour(displayedGem); if (tableTop.getTableTopPanel().isPhotoLook()) { g2d.setPaint(highlightColour); } else { Paint gradient2 = new GradientPaint(0, 0, highlightColour, 25, 32, baseColour, true); g2d.setPaint(gradient2); } g2d.fill(facetTri); g2d.setColor(baseColour); // NOTE: Using the default line width or thinner seems to cause the vertical left edge of the // gem and the vertical left edge of the facet triangle to be drawn improperly (or not at all) // when using JDK 1.4.0. Making the line width slightly greater than the default seems // to fix this problem??? // Set the stroke to have a line width that is slightly wider than the default. g2d.setStroke(new BasicStroke(1.000001f)); g2d.draw(facetTri); // Draw the three 'edges' g2d.drawLine(triAxs[0], triAys[0], triB1.x, triB1.y); g2d.drawLine(triAxs[1], triAys[1], triB2.x, triB2.y); g2d.drawLine(triAxs[2], triAys[2], triB3.x, triB3.y); // Draw the solid outline of the Gem if (tableTop.getTableTopPanel().isPhotoLook()) { g2d.setColor(baseColour); } else { g2d.setColor(Color.black); } g2d.draw(gemTri); } /** * Paint the body of the displayed gem as an triangle (emerald?) shape. * @param displayedGem * @param g2d */ private void paintBodyRectangle(DisplayedGem displayedGem, Graphics2D g2d) { // The main Gem rectangle Polygon gemRect = (Polygon)displayedGem.getBodyShape(); int[] rectAxs = gemRect.xpoints; int[] rectAys = gemRect.ypoints; // Fill the Gem Color baseColour = getTableTopBaseColour(); Paint gradient = new GradientPaint(0, 0, getGemColour(displayedGem), 15, 25, baseColour, true); g2d.setPaint(gradient); g2d.fill(gemRect); // Draw the 'facets' Polygon facetRect = new Polygon(); Point rectB1 = new Point(rectAxs[0] + DisplayConstants.BEVEL_WIDTH_X, rectAys[0] + DisplayConstants.BEVEL_WIDTH_Y); Point rectB2 = new Point(rectAxs[1] - DisplayConstants.BEVEL_WIDTH_X, rectAys[1] + DisplayConstants.BEVEL_WIDTH_Y); Point rectB3 = new Point(rectAxs[2] - DisplayConstants.BEVEL_WIDTH_X, rectAys[2] - DisplayConstants.BEVEL_WIDTH_Y); Point rectB4 = new Point(rectAxs[3] + DisplayConstants.BEVEL_WIDTH_X, rectAys[3] - DisplayConstants.BEVEL_WIDTH_Y); facetRect.addPoint(rectB1.x, rectB1.y); facetRect.addPoint(rectB2.x, rectB2.y); facetRect.addPoint(rectB3.x, rectB3.y); facetRect.addPoint(rectB4.x, rectB4.y); // Draw and fill the facet rectangle Paint gradient2 = new GradientPaint(0, 0, getGemHighlightColour(displayedGem), 25, 32, baseColour, true); g2d.setPaint(gradient2); g2d.fill(facetRect); g2d.setColor(baseColour); g2d.draw(facetRect); // Draw the four 'edges' g2d.drawLine(rectAxs[0], rectAys[0], rectB1.x, rectB1.y); g2d.drawLine(rectAxs[1], rectAys[1], rectB2.x, rectB2.y); g2d.drawLine(rectAxs[2], rectAys[2], rectB3.x, rectB3.y); g2d.drawLine(rectAxs[3], rectAys[3], rectB4.x, rectB4.y); // Draw the solid outline of the Gem if (tableTop.getTableTopPanel().isPhotoLook()) { g2d.setColor(DisplayConstants.TRANSPARENT_WHITE); } else { g2d.setColor(Color.black); } g2d.draw(gemRect); } /** * Paint the indicated input part on this displayed gem. * @param displayedGem the displayed gem in question. * @param inputIndex the index of the input to paint. * @param graphics2D the graphics context into which to paint. */ private void paintInputPart(DisplayedGem displayedGem, int inputIndex, Graphics2D graphics2D) { // Create a local copy of the graphics object, which we can modify safely. Graphics2D g2d = (Graphics2D)graphics2D.create(); // Set the colour based on the type g2d.setColor(tableTop.getTypeColour(displayedGem.getDisplayedInputPart(inputIndex))); Point connectPoint = displayedGem.getDisplayedGemShape().getInputConnectPoint(inputIndex); int x = connectPoint.x; int y = connectPoint.y; int inBoundsWidth = displayedGem.getDisplayedGemShape().getInBounds().width; Gem inputGem = displayedGem.getGem(); Gem.PartInput input = inputGem.getInputPart(inputIndex); // Draw the input burnt if it's, er... burnt if (input.isBurnt()) { // Burnt // Draw the 'split ends' effect g2d.setColor(Color.black); g2d.drawArc(x, y - DisplayConstants.ARGUMENT_SPACING/2, inBoundsWidth * 2, DisplayConstants.ARGUMENT_SPACING/2, -135, 45); g2d.drawArc(x, y, inBoundsWidth * 2, DisplayConstants.ARGUMENT_SPACING/2, 135, -45); } else { // Not burnt // The line g2d.drawLine((int)(x + DisplayConstants.BIND_POINT_RADIUS * 2.5), y, x + inBoundsWidth, y); // If the current inputPart that we are looking at is not the intellicutPart, or if we are not // pulsing, then we go with normal input drawing, else we draw the input a bit more emphasized. Shape inputShape; IntellicutManager intellicutManager = tableTop.getIntellicutManager(); if ((intellicutManager.getIntellicutPart() == displayedGem.getDisplayedInputPart(inputIndex)) && (intellicutManager.getIntellicutMode() == IntellicutMode.PART_INPUT)) { // Use special polygon for this input drawing. inputShape = getIntellicutInputShape(connectPoint); } else { // Normal input shape. inputShape = DisplayedGemShape.getInputShape(connectPoint); } // Antialias input rendering. // Also, set stroke control to pure, otherwise circles look deformed. g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE); if (input.isConnected()) { // fill in the shape. g2d.fill(inputShape); } else { CollectorGem inputArgumentTarget = GemGraph.getInputArgumentTarget(input); if (inputArgumentTarget == tableTop.getTargetCollector()) { // fill in the shape, and circle it red. g2d.fill(inputShape); g2d.setColor(Color.RED); drawTargetMarker(connectPoint, g2d); } else { CollectorGem rootCollector = input.getGem().getRootCollectorGem(); boolean isReflecting = rootCollector != null && rootCollector == inputArgumentTarget; if (isReflecting) { // Just draw the outline of the shape. g2d.draw(inputShape); } else { // fill in the shape. g2d.fill(inputShape); // circle it black. if (inputArgumentTarget != null) { g2d.setColor(Color.BLACK); drawTargetMarker(connectPoint, g2d); } } } } // Highlight if photolook if (tableTop.getTableTopPanel().isPhotoLook()) { g2d.setColor(getTableTopBaseColour()); g2d.draw(inputShape); } } g2d.dispose(); } /** * Draw the Shape representing the target marker for this Gem's nth input. * @param bindPoint the location of the bind point * @param g2d the graphics object to use. */ private void drawTargetMarker(Point bindPoint, Graphics2D g2d) { // Copy the graphics object g2d = (Graphics2D)g2d.create(); // Note: we want to use drawOval(), which takes int arguments. // However, we want to draw in between pixels, so we will multiply all coordinates by two, and scale by 0.5. g2d.scale(0.5, 0.5); int x = (2 * bindPoint.x); int y = (2 * bindPoint.y) - (DisplayConstants.BIND_POINT_RADIUS * 3); // 3 == 1.5 * 2; int diameter = DisplayConstants.BIND_POINT_RADIUS * 6; // 6 == 3 * 2 // Draw the shape. g2d.drawOval(x, y, diameter, diameter); // Dispose the graphics copy. g2d.dispose(); } /** * Get the shape representing an input when affected by Intellicut. * @param bindPoint the location of the bind point * @return Shape the shape of the bind point */ private Shape getIntellicutInputShape(Point bindPoint){ int radius = (int)(0.75 * DisplayConstants.BIND_POINT_RADIUS); // Where does this blob go vertically int x = bindPoint.x + radius; int y = bindPoint.y; Polygon diamond = new Polygon(); diamond.addPoint(x + radius, y - 2 * radius); diamond.addPoint(x - radius, y); diamond.addPoint(x + radius, y + 2 * radius); diamond.addPoint(x + 3 * radius, y); return diamond; } /** * Paint the output part if any. * @param displayedGem DisplayedGem the gem to paint * @param g2d Graphics2D the graphics context */ private void paintOutputPart(DisplayedGem displayedGem, Graphics2D g2d) { DisplayedPartOutput displayedOutputPart = displayedGem.getDisplayedOutputPart(); if (displayedOutputPart == null) { return; } Color prevColour = g2d.getColor(); Rectangle outRect = displayedGem.getDisplayedGemShape().getOutBounds(); int centreY = outRect.y + (outRect.height/2); // Draw the output line and bind point if output connected (colour based on output type) g2d.setColor(tableTop.getTypeColour(displayedOutputPart)); g2d.drawLine(outRect.x - 1, centreY, outRect.x + outRect.width, centreY); Polygon bindPoint = displayedGem.getDisplayedGemShape().getOutputShape(); g2d.fill(bindPoint); if (tableTop.getTableTopPanel().isPhotoLook()) { // Highlight arrow g2d.setColor(DisplayConstants.TRANSPARENT_WHITE); g2d.draw(bindPoint); } g2d.setColor(prevColour); } /** * Paint a Connection in the given graphics context * @param dConn DisplayedConnection the connection to paint * @param g2d Graphics2D the graphics context */ void paintConnection(DisplayedConnection dConn, Graphics2D g2d) { // Make a ConnectionRoute ConnectionRoute route = dConn.getConnectionRoute(); // Get the ConnectionRoute's bounding rectangle Rectangle bounds = route.getBoundingRectangle(); if (bounds.intersects(g2d.getClipBounds())) { // Need to draw this connection // The colour is the type colour of the source part if (!tableTop.isBadDisplayedConnection(dConn)) { g2d.setColor(tableTop.getTypeColour(dConn.getSource())); } else { g2d.setColor(Color.red); } // Finally draw it route.draw(g2d); } } /** * Get the base colour used for the given displayed gem. * @param displayedGem the displayed gem in question. * @return the gem colour. */ private Color getBaseGemColour(DisplayedGem displayedGem) { Gem gem = displayedGem.getGem(); if (gem instanceof CodeGem) { // Code gems are green. return Color.green; } else if (gem instanceof CollectorGem) { // Collectors are black. return Color.BLACK; } else if (gem instanceof RecordFieldSelectionGem) { // RecordFieldSelection gems are indigo-ish. return Color.decode("0x8000F0"); } else if (gem instanceof RecordCreationGem) { // Record creation gems are magenta return Color.MAGENTA; } else if (gem instanceof FunctionalAgentGem) { // Data constructors are orange. if (((FunctionalAgentGem)gem).getGemEntity().isDataConstructor()) { return Color.orange; } // Other functional agents are red return Color.red; } else if (gem instanceof ReflectorGem) { // Reflectors reflecting the target are pink (somewhere between red and white).. if (((ReflectorGem)gem).getCollector() == tableTop.getTargetCollector()) { return Color.pink; } // Normal reflectors are white. return Color.white; } else if (gem instanceof ValueGem) { // if the value gem is not editable display it in gray if (!tableTop.getTableTopPanel().getValueEntryPanel((ValueGem)gem).isEditable()) { return Color.gray; } // editable value gems are blue return Color.blue; } else { throw new IllegalArgumentException("Unrecognized displayed gem type: " + displayedGem.getClass()); } } /** * Get the colour used as the body colour for the given displayed gem. * @param displayedGem the displayed gem in question. * @return the gem colour. */ private Color getGemColour(DisplayedGem displayedGem) { Color baseGemColour = getBaseGemColour(displayedGem); if (tableTop.getTableTopPanel().isPhotoLook()) { return GemCutterPaintHelper.getAlphaColour(baseGemColour, DisplayConstants.GEM_FACET_TRANSPARENCY); } else { return baseGemColour; } } /** * Get the colour used as the base colour for the tabletop. * @return the base colour. */ private Color getTableTopBaseColour() { if (tableTop.getTableTopPanel().isPhotoLook()) { return DisplayConstants.TRANSPARENT_WHITE; } else { return Color.WHITE; } } /** * Get the colour used as the base colour for the given displayed gem. * @param displayedGem the displayed gem in question. * @return the base colour. */ private Color getCrackColour(DisplayedGem displayedGem) { if (tableTop.getTableTopPanel().isPhotoLook()) { if (displayedGem.getGem() instanceof ReflectorGem) { return GemCutterPaintHelper.getAlphaColour(Color.LIGHT_GRAY, DisplayConstants.GEM_TRANSPARENCY); } else { return getTableTopBaseColour(); } } else { return PLAIN_CRACK_COLOUR; } } /** * Get the colour used for highlighting the given displayed gem. * @param displayedGem the displayed gem in question. * @return the highlight colour. */ private Color getGemHighlightColour(DisplayedGem displayedGem) { // Need to come up with a lighter colour of gemColour for highlighting purposes, // and as the inner colour of the Gem (to make it easier to read the Gem name). // TEMP: Michael - Need to come up with a better way of brightening up colours. Color baseGemColour = getBaseGemColour(displayedGem); Color gemHighlightColour = new Color(Math.max(110, baseGemColour.getRed()), Math.max(110, baseGemColour.getGreen()), Math.max(110, baseGemColour.getBlue())); if (tableTop.getTableTopPanel().isPhotoLook()) { gemHighlightColour = GemCutterPaintHelper.getAlphaColour(gemHighlightColour, DisplayConstants.GEM_TRANSPARENCY); } return gemHighlightColour; } /** * Get the colour used for the halo for the given displayed gem. * @param displayedGem the displayed gem in question. * @return the halo colour. */ private Color getGemHaloColour(DisplayedGem displayedGem) { if (tableTop.getTableTopPanel().isPhotoLook()) { return DisplayConstants.TRANSPARENT_WHITE; } else { return getGemHighlightColour(displayedGem); } } /** * Return the color with which to display the gem's text. * @param displayedGem the displayed gem in question. * @return the gem colour. */ private static Color getTextColour(DisplayedGem displayedGem) { if (displayedGem.getGem() instanceof CollectorGem) { return Color.WHITE; } else { return Color.BLACK; } } /** * {@inheritDoc} */ public void gemLocationChanged(DisplayedGemLocationEvent e) { // Invalidate cache. Only really needed for input/output gems // Our local use of absolute coords in the 'crazy paving' for broken FunctionalAgentGems // forces us to override this and flush the cached object on a move as well as a resize. DisplayedGem dGemSource = (DisplayedGem)e.getSource(); crazyPavingCache.remove(dGemSource); } /** * {@inheritDoc} */ public void gemSizeChanged(DisplayedGemSizeEvent e) { DisplayedGem displayedGem = (DisplayedGem)e.getSource(); tapeLabelCache.remove(displayedGem); runHaloCache.remove(displayedGem); selectHaloCache.remove(displayedGem); } }