/* * DragAndDropFragmentBinPanel.java * * Created on 11 September 2006, 11:01 */ package uk.co.bytemark.vm.enigma.inquisition.gui.quiz; import java.awt.Color; import java.awt.Dimension; import java.awt.Font; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Point; import java.awt.Shape; import java.awt.font.FontRenderContext; import java.awt.geom.Rectangle2D; import java.awt.geom.RoundRectangle2D; import java.awt.image.BufferedImage; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.swing.JPanel; import uk.co.bytemark.vm.enigma.inquisition.questions.DragAndDropQuestion; /** * A "bin" of fragments for a {@link DragAndDropQuestion}. * * @see DragAndDropPanel */ class DragAndDropFragmentBinPanel extends JPanel { private int preferredSizeY = 50; private int preferredSizeX = 300; // The list of fragments (todo: bundle up this, paintFragment, fragmentRectangles and fragmentImages into a data // structure). private List<String> fragments; // Whether the each fragment is "in" the fragment bin private boolean[] paintFragment; // The location of each fragment in the bin is represented as a rectangle private Rectangle2D[] fragmentRectangles; // A mapping between a fragment and an image representing it private Map<String, BufferedImage> fragmentImages = new HashMap<String, BufferedImage>(); private boolean isBinActive = true; // The bin is inactive in review mode // Drawing constants private static final int SPACING = 10; private static final int START_X = 25; private static final int START_Y = 25; private static final int SHADOW_X = 5; private static final int SHADOW_Y = 5; private static final int WIDTH_PADDING = 20; private static final int CORNER_RADIUS = 10; private static final Color SHADOW_COLOUR = Color.GRAY; private static final Color TEXT_COLOUR = Color.BLACK; private static final Color INACTIVE_TEXT_COLOUR = Color.darkGray; public static final Color FRAGMENT_FILL_COLOUR = new Color(0.9f, 0.9f, 1.0f); private static final Color INACTIVE_FRAGMENT_FILL_COLOUR = new Color(0.915f, 0.9f, 0.9f); /** * Creates a new DragAndDropFragmentBinPanel. * * @param fragments * the list of fragments to be used in the question. The constructor will make a copy of the list, and * then randomly shuffle the order. */ public DragAndDropFragmentBinPanel(List<String> fragments) { this.fragments = new ArrayList<String>(fragments); Collections.shuffle(this.fragments); // Randomise order fragmentRectangles = new Rectangle2D[fragments.size()]; paintFragment = new boolean[fragments.size()]; for (int i = 0; i < fragments.size(); i++) paintFragment[i] = true; // Create a store of fragment images for use in drag-and-drop for (String fragment : fragments) fragmentImages.put(fragment, createImage(fragment)); } /** * Paints this component, and also calculates a new preferred height from its given width while doing so. */ @Override protected void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D g2 = (Graphics2D) g; Rectangle2D.Float rectangle; int maxX = getWidth(); int currentX = START_X; int currentY = START_Y; // The maximum height for this row of fragments // (these is very likely the same for all fragments at present) int maxHeight = 0; // Loop over each fragment for (int i = 0; i < fragments.size(); i++) { // Skip fragments that have been moved out of the bin if (!paintFragment[i]) { fragmentRectangles[i] = null; continue; } String fragment = fragments.get(i); // Precalculate the width before drawing int width = fragmentWidth(g2, fragment); // If the new fragment takes us passed the maximum width, // skip to the next row if (currentX + width > maxX - 10) { currentX = START_X; currentY += maxHeight + SPACING; maxHeight = 0; } // Draw the fragment, and stash its bounds rectangle = drawFragment(g2, fragment, currentX, currentY); fragmentRectangles[i] = rectangle; // Update drawing postion ready for next fragment currentX += ((int) rectangle.width) + SPACING; if (rectangle.height > maxHeight) maxHeight = (int) rectangle.height; } // We now know our new preferred size preferredSizeX = maxX; preferredSizeY = currentY + 50; // Call this here to make first appearance in frame work revalidate(); } /** * Returns the dimensions needed to render this fragment. */ private Rectangle2D.Float rectangleNeededFor(String fragment) { // Create a throwaway Graphics2D to trial-render this component BufferedImage image = new BufferedImage(2048, 200, BufferedImage.TYPE_INT_ARGB); Graphics2D g = (Graphics2D) image.getGraphics(); return drawFragment(g, fragment); } /** * Renders a fragment. */ private Rectangle2D.Float drawFragment(Graphics2D g, String fragmentText, int inX, int inY) { Font font = new Font("Monospaced", Font.PLAIN, 12); FontRenderContext fontRenderContext = g.getFontRenderContext(); Rectangle2D stringBoundsRectangle = font.getStringBounds(fragmentText, fontRenderContext); int fragmentWidth = (int) (stringBoundsRectangle.getWidth()); int fragmentHeight = (int) (stringBoundsRectangle.getHeight()); int x = inX + WIDTH_PADDING / 2; int y = inY + fragmentHeight; Shape fragmentShape = new RoundRectangle2D.Float(x - WIDTH_PADDING / 2, y - fragmentHeight, fragmentWidth + WIDTH_PADDING, fragmentHeight + fragmentHeight / 2, CORNER_RADIUS, CORNER_RADIUS); // The shadow is the same size as the fragment rectangle, // but shifted SHADOW_X/Y pixels across Shape shadow = new RoundRectangle2D.Float(x - WIDTH_PADDING / 2 + SHADOW_X, y - fragmentHeight + SHADOW_Y, fragmentWidth + WIDTH_PADDING, fragmentHeight + fragmentHeight / 2, CORNER_RADIUS, CORNER_RADIUS); g.setPaint(SHADOW_COLOUR); g.fill(shadow); if (isBinActive) { g.setPaint(FRAGMENT_FILL_COLOUR); g.fill(fragmentShape); g.setPaint(TEXT_COLOUR); g.draw(fragmentShape); } else { g.setPaint(INACTIVE_FRAGMENT_FILL_COLOUR); g.fill(fragmentShape); g.setPaint(INACTIVE_TEXT_COLOUR); g.draw(fragmentShape); } g.setFont(font); g.drawString(fragmentText, x, y); return new Rectangle2D.Float(inX, inY, fragmentWidth + WIDTH_PADDING + SHADOW_X, (int) (fragmentHeight * 1.5) + SHADOW_Y); } /** * Draws a fragment (convenience overloaded function) */ private Rectangle2D.Float drawFragment(Graphics2D graphics2D, String f) { return drawFragment(graphics2D, f, 0, 0); } /** * Precalculate fragment width Fixme: violates "don't repeat yourself" */ private int fragmentWidth(Graphics2D g, String s) { Font font = new Font("Monospaced", Font.PLAIN, 12); FontRenderContext frc = g.getFontRenderContext(); Rectangle2D r = font.getStringBounds(s, frc); int fWidth = (int) (r.getWidth()); return fWidth + WIDTH_PADDING + SHADOW_X; } @Override public Dimension getPreferredSize() { return new Dimension(preferredSizeX, preferredSizeY); } /** * Returns the fragment at this point in the fragment bin (if there's one active) */ String getFragmentAtPoint(Point point) { for (int i = 0; i < fragments.size(); i++) { Rectangle2D fragmentRectangle = fragmentRectangles[i]; if (paintFragment[i] && fragmentRectangle != null && fragmentRectangle.contains(point.x, point.y)) return fragments.get(i); } return null; } /** * Returns the index of the fragment in the list of fragments. */ private int findIndex(String f) { int index = -1; for (int i = 0; i < fragments.size(); i++) { if (f.equals(fragments.get(i))) { index = i; break; } } return index; } void hideFragment(String f) { int index = findIndex(f); if (index >= 0) paintFragment[index] = false; repaint(); } void showFragment(String f) { int index = findIndex(f); if (index >= 0) paintFragment[index] = true; repaint(); } /** * Fetches a cached <tt>BufferedImage</tt> representing the given fragment. */ BufferedImage getFragmentImage(String f) { return fragmentImages.get(f); } /** * Returns a new <tt>BufferedImage</tt> representing the given fragment. */ private BufferedImage createImage(String f) { Rectangle2D r = rectangleNeededFor(f); BufferedImage image = new BufferedImage((int) r.getWidth(), (int) r.getHeight(), BufferedImage.TYPE_INT_ARGB); Graphics2D g = (Graphics2D) image.getGraphics(); drawFragment(g, f); return image; } void setActive(boolean active) { isBinActive = active; } }