/* * @(#)DefaultDrawingView.java * * Copyright (c) 1996-2010 The authors and contributors of JHotDraw. * You may not use, copy or modify this file, except in compliance with the * accompanying license terms. */ package org.jhotdraw.draw; import javax.annotation.Nullable; import org.jhotdraw.draw.event.FigureSelectionEvent; import org.jhotdraw.draw.event.FigureSelectionListener; import org.jhotdraw.draw.handle.Handle; import org.jhotdraw.draw.event.HandleListener; import org.jhotdraw.draw.event.HandleEvent; import org.jhotdraw.draw.event.FigureListener; import org.jhotdraw.draw.event.FigureAdapter; import org.jhotdraw.draw.event.FigureEvent; import org.jhotdraw.draw.event.CompositeFigureListener; import org.jhotdraw.draw.event.CompositeFigureEvent; import javax.swing.undo.*; import org.jhotdraw.util.*; import java.awt.*; import java.awt.geom.*; import java.awt.event.*; import java.awt.image.BufferedImage; import java.util.*; import javax.swing.*; import org.jhotdraw.gui.EditableComponent; import static org.jhotdraw.draw.AttributeKeys.*; import java.awt.image.VolatileImage; /** * A default implementation of {@link DrawingView} suited for viewing drawings with a small number * of figures. * * FIXME - Implement clone Method. FIXME - Use double buffering for the drawing to improve * performance. * * @author Werner Randelshofer * @version $Id$ */ public class DefaultDrawingView extends JComponent implements DrawingView, EditableComponent { private static final long serialVersionUID = 1L; /** * Set this to true to turn on debugging output on System.out. */ private static final boolean DEBUG = false; @Nullable private Drawing drawing; /** * Holds the selected figures in an ordered put. The ordering reflects the sequence that was * used to select the figures. */ private Set<Figure> selectedFigures = new LinkedHashSet<>(); private LinkedList<Handle> selectionHandles = new LinkedList<>(); private boolean isConstrainerVisible = false; private Constrainer visibleConstrainer = new GridConstrainer(8, 8); private Constrainer invisibleConstrainer = new GridConstrainer(); private Handle secondaryHandleOwner; @Nullable private Handle activeHandle; private LinkedList<Handle> secondaryHandles = new LinkedList<>(); private boolean handlesAreValid = true; @Nullable private transient Dimension cachedPreferredSize; private double scaleFactor = 1; private Point translation = new Point(0, 0); private int detailLevel; @Nullable private DrawingEditor editor; private JLabel emptyDrawingLabel; protected BufferedImage backgroundTile; private FigureListener handleInvalidator = new FigureAdapter() { @Override public void figureHandlesChanged(FigureEvent e) { invalidateHandles(); } }; @Nullable private transient Rectangle2D.Double cachedDrawingArea; public static final String DRAWING_DOUBLE_BUFFERED_PROPERTY = "drawingDoubleBuffered"; /** * Whether the drawing is double buffered */ private boolean isDrawingDoubleBuffered = true; /** * The drawingBuffer holds a rendered image of the drawing (in view coordinates). */ @Nullable private VolatileImage drawingBufferV; /** * The drawingBuffer holds a rendered image of the drawing (in view coordinates). */ @Nullable private BufferedImage drawingBufferNV; /** * Holds the drawing area (in view coordinates) which is in the drawing buffer. */ private Rectangle bufferedArea = new Rectangle(); /** * Holds the drawing area (in view coordinates) which has not been redrawn yet in the drawing * buffer. */ private Rectangle dirtyArea = new Rectangle(0, 0, -1, -1); private boolean paintEnabled = true; private static final boolean isWindows; static { boolean b = false; try { if (System.getProperty("os.name").toLowerCase().startsWith("win")) { b = true; } } catch (Throwable t) { } isWindows = b; } @Override public void repaintHandles() { validateHandles(); Rectangle r = null; for (Handle h : getSelectionHandles()) { if (r == null) { r = h.getDrawingArea(); } else { r.add(h.getDrawingArea()); } } for (Handle h : getSecondaryHandles()) { if (r == null) { r = h.getDrawingArea(); } else { r.add(h.getDrawingArea()); } } if (r != null) { repaint(r); } } /** * Draws the background of the drawing view. */ protected void drawBackground(Graphics2D g) { if (drawing == null) { // there is no drawing and thus no canvas g.setColor(getBackground()); g.fillRect(0, 0, getWidth(), getHeight()); } else if (drawing.get(CANVAS_WIDTH) == null || drawing.get(CANVAS_HEIGHT) == null) { // the canvas is infinitely large Color canvasColor = drawing.get(CANVAS_FILL_COLOR); double canvasOpacity = drawing.get(CANVAS_FILL_OPACITY); if (canvasColor != null) { if (canvasOpacity == 1) { g.setColor(new Color(canvasColor.getRGB())); g.fillRect(0, 0, getWidth(), getHeight()); } else { Point r = drawingToView(new Point2D.Double(0, 0)); g.setPaint(getBackgroundPaint(r.x, r.y)); g.fillRect(0, 0, getWidth(), getHeight()); g.setColor(new Color(canvasColor.getRGB() & 0xfffff | ((int) (canvasOpacity * 256) << 24), true)); g.fillRect(0, 0, getWidth(), getHeight()); } } else { Point r = drawingToView(new Point2D.Double(0, 0)); g.setPaint(getBackgroundPaint(r.x, r.y)); g.fillRect(0, 0, getWidth(), getHeight()); } } else { // the canvas has a fixed size g.setColor(getBackground()); g.fillRect(0, 0, getWidth(), getHeight()); Rectangle r = drawingToView(new Rectangle2D.Double(0, 0, drawing.get(CANVAS_WIDTH), drawing.get(CANVAS_HEIGHT))); g.setPaint(getBackgroundPaint(r.x, r.y)); g.fillRect(r.x, r.y, r.width, r.height); } } @Override public boolean isSelectionEmpty() { return selectedFigures.isEmpty(); } private class EventHandler implements FigureListener, CompositeFigureListener, HandleListener, FocusListener { @Override public void figureAdded(CompositeFigureEvent evt) { if (drawing.getChildCount() == 1 && getEmptyDrawingMessage() != null) { repaint(); } else { repaintDrawingArea(evt.getInvalidatedArea()); } invalidateDimension(); } @Override public void figureRemoved(CompositeFigureEvent evt) { if (drawing.getChildCount() == 0 && getEmptyDrawingMessage() != null) { repaint(); } else { repaintDrawingArea(evt.getInvalidatedArea()); } removeFromSelection(evt.getChildFigure()); invalidateDimension(); } @Override public void areaInvalidated(FigureEvent evt) { repaintDrawingArea(evt.getInvalidatedArea()); invalidateDimension(); } @Override public void areaInvalidated(HandleEvent evt) { repaint(evt.getInvalidatedArea()); invalidateDimension(); } @Override public void handleRequestSecondaryHandles(HandleEvent e) { secondaryHandleOwner = e.getHandle(); secondaryHandles.clear(); secondaryHandles.addAll(secondaryHandleOwner.createSecondaryHandles()); for (Handle h : secondaryHandles) { h.setView(DefaultDrawingView.this); h.addHandleListener(eventHandler); } repaint(); } @Override public void focusGained(FocusEvent e) { // repaintHandles(); if (editor != null) { editor.setActiveView(DefaultDrawingView.this); } } @Override public void focusLost(FocusEvent e) { // repaintHandles(); } @Override public void handleRequestRemove(HandleEvent e) { selectionHandles.remove(e.getHandle()); e.getHandle().dispose(); invalidateHandles(); repaint(e.getInvalidatedArea()); } @Override public void attributeChanged(FigureEvent e) { if (e.getSource() == drawing) { AttributeKey<?> a = e.getAttribute(); if (a.equals(CANVAS_HEIGHT) || a.equals(CANVAS_WIDTH)) { validateViewTranslation(); repaint(); // must repaint everything } if (e.getInvalidatedArea() != null) { repaintDrawingArea(e.getInvalidatedArea()); } else { repaintDrawingArea(viewToDrawing(getCanvasViewBounds())); } } else { if (e.getInvalidatedArea() != null) { repaintDrawingArea(e.getInvalidatedArea()); } } } @Override public void figureHandlesChanged(FigureEvent e) { } @Override public void figureChanged(FigureEvent e) { repaintDrawingArea(e.getInvalidatedArea()); } @Override public void figureAdded(FigureEvent e) { } @Override public void figureRemoved(FigureEvent e) { } @Override public void figureRequestRemove(FigureEvent e) { } } private EventHandler eventHandler; /** * Creates new instance. */ public DefaultDrawingView() { initComponents(); eventHandler = createEventHandler(); setToolTipText("dummy"); // Set a dummy tool tip text to turn tooltips on setFocusable(true); addFocusListener(eventHandler); setTransferHandler(new DefaultDrawingViewTransferHandler()); setBackground(new Color(0xb0b0b0)); setOpaque(true); } protected EventHandler createEventHandler() { return new EventHandler(); } /** * This method is called from within the constructor to initialize the form.<p> * WARNING: Do NOT modify this code. The content of this method is always regenerated by the * Form Editor.<p> * NOTE: To prevent undesired layout effects when using floating text fields, the * DefaultDrawingView must not use a layout manager. */ // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents private void initComponents() { setLayout(null); }// </editor-fold>//GEN-END:initComponents @Override @Nullable public Drawing getDrawing() { return drawing; } @Override public String getToolTipText(MouseEvent evt) { if (getEditor() != null && getEditor().getTool() != null) { return getEditor().getTool().getToolTipText(this, evt); } return null; } public void setEmptyDrawingMessage(String newValue) { String oldValue = (emptyDrawingLabel == null) ? null : emptyDrawingLabel.getText(); if (newValue == null) { emptyDrawingLabel = null; } else { emptyDrawingLabel = new JLabel(newValue); emptyDrawingLabel.setHorizontalAlignment(JLabel.CENTER); } firePropertyChange("emptyDrawingMessage", oldValue, newValue); repaint(); } public String getEmptyDrawingMessage() { return (emptyDrawingLabel == null) ? null : emptyDrawingLabel.getText(); } /** * Paints the drawing view. Uses rendering hints for fast painting. Paints the canvasColor, the * grid, the drawing, the handles and the current tool. */ @Override public void paintComponent(Graphics gr) { Graphics2D g = (Graphics2D) gr; setViewRenderingHints(g); drawBackground(g); drawCanvas(g); drawConstrainer(g); if (isDrawingDoubleBuffered()) { if (isWindows) { drawDrawingNonvolatileBuffered(g); } else { drawDrawingVolatileBuffered(g); } } else { drawDrawing(g); } drawHandles(g); drawTool(g); } /** * Draws the drawing double buffered using a volatile image. */ protected void drawDrawingVolatileBuffered(Graphics2D g) { Rectangle vr = getVisibleRect(); Point shift = new Point(0, 0); if (bufferedArea.contains(vr) || bufferedArea.width >= vr.width && bufferedArea.height >= vr.height) { // The visible rect fits into the buffered area, but may be shifted; shift the buffered area. shift.x = bufferedArea.x - vr.x; shift.y = bufferedArea.y - vr.y; if (shift.x > 0) { dirtyArea.add(new Rectangle(bufferedArea.x - shift.x, vr.y, shift.x + bufferedArea.width - vr.width, bufferedArea.height)); } else if (shift.x < 0) { dirtyArea.add(new Rectangle(bufferedArea.x + vr.width, vr.y, -shift.x + bufferedArea.width - vr.width, bufferedArea.height)); } if (shift.y > 0) { dirtyArea.add(new Rectangle(vr.x, bufferedArea.y - shift.y, bufferedArea.width, shift.y + bufferedArea.height - vr.height)); } else if (shift.y < 0) { dirtyArea.add(new Rectangle(vr.x, bufferedArea.y + vr.height, bufferedArea.width, -shift.y + bufferedArea.height - vr.height)); } bufferedArea.x = vr.x; bufferedArea.y = vr.y; } else { // The buffered drawing area does not match the visible rect; // resize it, and mark everything as dirty. bufferedArea.setBounds(vr); dirtyArea.setBounds(vr); if (drawingBufferV != null && // (drawingBufferV.getWidth() != vr.width || drawingBufferV.getHeight() != vr.height)) { // The dimension of the drawing buffer does not fit into the visible rect; // throw the buffer away. drawingBufferV.flush(); drawingBufferV = null; } } // Update the contents of the buffer if necessary while (true) { int valid = (drawingBufferV == null) ? // VolatileImage.IMAGE_INCOMPATIBLE : // drawingBufferV.validate(getGraphicsConfiguration()); switch (valid) { case VolatileImage.IMAGE_INCOMPATIBLE: // old buffer doesn't work with new GraphicsConfig; (re-)create it try { drawingBufferV = getGraphicsConfiguration().createCompatibleVolatileImage(vr.width, vr.height, Transparency.TRANSLUCENT); } catch (OutOfMemoryError e) { drawingBufferV = null; } dirtyArea.setBounds(bufferedArea); break; case VolatileImage.IMAGE_RESTORED: // image was restored, but buffer lost; redraw everything dirtyArea.setBounds(bufferedArea); break; } if (drawingBufferV == null) { // There is not enough memory available for a drawing buffer; // draw without buffering. drawDrawing(g); break; } if (!dirtyArea.isEmpty()) { // An area of the drawing buffer is dirty; repaint it Graphics2D gBuf = drawingBufferV.createGraphics(); setViewRenderingHints(gBuf); // For shifting and cleaning, we need to erase everything underneath gBuf.setComposite(AlphaComposite.Src); // Perform shifting if needed if (shift.x != 0 || shift.y != 0) { gBuf.copyArea(Math.max(0, -shift.x), Math.max(0, -shift.y), drawingBufferV.getWidth() - Math.abs(shift.x), drawingBufferV.getHeight() - Math.abs(shift.y), shift.x, shift.y); shift.x = shift.y = 0; } // Clip the dirty area gBuf.translate(-bufferedArea.x, -bufferedArea.y); gBuf.clip(dirtyArea); // Clear the dirty area gBuf.setBackground(new Color(0x0, true)); gBuf.clearRect(dirtyArea.x, dirtyArea.y, dirtyArea.width, dirtyArea.height); gBuf.setComposite(AlphaComposite.SrcOver); // Repaint the dirty area drawDrawing(gBuf); gBuf.dispose(); } if (!drawingBufferV.contentsLost()) { g.drawImage(drawingBufferV, bufferedArea.x, bufferedArea.y, null); } if (drawingBufferV.contentsLost()) { dirtyArea.setBounds(bufferedArea); } else { dirtyArea.setSize(-1, -1); break; } } } /** * Draws the drawing double buffered using a buffered image. */ protected void drawDrawingNonvolatileBuffered(Graphics2D g) { Rectangle vr = getVisibleRect(); Point shift = new Point(0, 0); if (bufferedArea.contains(vr) || bufferedArea.width >= vr.width && bufferedArea.height >= vr.height) { // The visible rect fits into the buffered area, but may be shifted; shift the buffered area. shift.x = bufferedArea.x - vr.x; shift.y = bufferedArea.y - vr.y; if (shift.x > 0) { dirtyArea.add(new Rectangle(bufferedArea.x - shift.x, vr.y, shift.x + bufferedArea.width - vr.width, bufferedArea.height)); } else if (shift.x < 0) { dirtyArea.add(new Rectangle(bufferedArea.x + vr.width, vr.y, -shift.x + bufferedArea.width - vr.width, bufferedArea.height)); } if (shift.y > 0) { dirtyArea.add(new Rectangle(vr.x, bufferedArea.y - shift.y, bufferedArea.width, shift.y + bufferedArea.height - vr.height)); } else if (shift.y < 0) { dirtyArea.add(new Rectangle(vr.x, bufferedArea.y + vr.height, bufferedArea.width, -shift.y + bufferedArea.height - vr.height)); } bufferedArea.x = vr.x; bufferedArea.y = vr.y; } else { // The buffered drawing area does not match the visible rect; // resize it, and mark everything as dirty. bufferedArea.setBounds(vr); dirtyArea.setBounds(vr); if (drawingBufferNV != null && // (drawingBufferNV.getWidth() != vr.width || drawingBufferNV.getHeight() != vr.height)) { // The dimension of the drawing buffer does not fit into the visible rect; // throw the buffer away. drawingBufferNV.flush(); drawingBufferNV = null; } } // Update the contents of the buffer if necessary int valid = (drawingBufferNV == null) ? // VolatileImage.IMAGE_INCOMPATIBLE : VolatileImage.IMAGE_OK; switch (valid) { case VolatileImage.IMAGE_INCOMPATIBLE: // old buffer doesn't work with new GraphicsConfig; (re-)create it try { drawingBufferNV = getGraphicsConfiguration().createCompatibleImage(vr.width, vr.height, Transparency.TRANSLUCENT); } catch (OutOfMemoryError e) { drawingBufferNV = null; } dirtyArea.setBounds(bufferedArea); break; } if (drawingBufferNV == null) { // There is not enough memory available for a drawing buffer; // draw without buffering. drawDrawing(g); return; } if (!dirtyArea.isEmpty()) { // An area of the drawing buffer is dirty; repaint it Graphics2D gBuf = drawingBufferNV.createGraphics(); setViewRenderingHints(gBuf); // For shifting and cleaning, we need to erase everything underneath gBuf.setComposite(AlphaComposite.Src); // Perform shifting if needed if (shift.x != 0 || shift.y != 0) { gBuf.copyArea(Math.max(0, -shift.x), Math.max(0, -shift.y), drawingBufferNV.getWidth() - Math.abs(shift.x), drawingBufferNV.getHeight() - Math.abs(shift.y), shift.x, shift.y); shift.x = shift.y = 0; } // Clip the dirty area gBuf.translate(-bufferedArea.x, -bufferedArea.y); gBuf.clip(dirtyArea); // Clear the dirty area gBuf.setBackground(new Color(0x0, true)); gBuf.clearRect(dirtyArea.x, dirtyArea.y, dirtyArea.width, dirtyArea.height); gBuf.setComposite(AlphaComposite.SrcOver); // Repaint the dirty area drawDrawing(gBuf); gBuf.dispose(); } g.drawImage(drawingBufferNV, bufferedArea.x, bufferedArea.y, null); dirtyArea.setSize(-1, -1); } /** * Prints the drawing view. Uses high quality rendering hints for printing. Only prints the * drawing. Doesn't print the canvasColor, the grid, the handles and the tool. */ @Override public void printComponent(Graphics gr) { Graphics2D g = (Graphics2D) gr; // Set rendering hints for quality g.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY); g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_NORMALIZE); g.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON); g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); drawDrawing(g); } protected void setViewRenderingHints(Graphics2D g) { // Set rendering hints for speed g.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY); g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_NORMALIZE); g.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON); g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR); g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED); g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); } /** * Returns the bounds of the canvas on the drawing view. * * @return The current bounds of the canvas on the drawing view. */ protected Rectangle getCanvasViewBounds() { // Position of the zero coordinate point on the view int x = -translation.x; int y = -translation.y; int w = getWidth(); int h = getHeight(); if (getDrawing() != null) { Double cw = getDrawing().get(CANVAS_WIDTH); Double ch = getDrawing().get(CANVAS_HEIGHT); if (cw != null && ch != null) { Point lowerRight = drawingToView( new Point2D.Double(cw, ch)); w = lowerRight.x - x; h = lowerRight.y - y; } } return new Rectangle(x, y, w, h); } /** * Draws the canvas. If the {@code AttributeKeys.CANVAS_FILL_OPACITY} is not fully opaque, the * canvas area is filled with the background paint before the * {@code AttributeKeys.CANVAS_FILL_COLOR} is drawn. */ protected void drawCanvas(Graphics2D gr) { if (drawing != null) { Graphics2D g = (Graphics2D) gr.create(); AffineTransform tx = g.getTransform(); tx.translate(-translation.x, -translation.y); tx.scale(scaleFactor, scaleFactor); g.setTransform(tx); drawing.setFontRenderContext(g.getFontRenderContext()); drawing.drawCanvas(g); g.dispose(); } } protected void drawConstrainer(Graphics2D g) { if (getConstrainer() != null) { Shape clip = g.getClip(); Rectangle r = getCanvasViewBounds(); g.clipRect(r.x, r.y, r.width, r.height); getConstrainer().draw(g, this); g.setClip(clip); } } protected void drawDrawing(Graphics2D gr) { if (drawing != null) { if (drawing.getChildCount() == 0 && emptyDrawingLabel != null) { emptyDrawingLabel.setBounds(0, 0, getWidth(), getHeight()); emptyDrawingLabel.paint(gr); } else { Graphics2D g = (Graphics2D) gr.create(); AffineTransform tx = g.getTransform(); tx.translate(-translation.x, -translation.y); tx.scale(scaleFactor, scaleFactor); g.setTransform(tx); drawing.setFontRenderContext(g.getFontRenderContext()); drawing.draw(g); g.dispose(); } } } protected void drawHandles(java.awt.Graphics2D g) { if (editor != null && editor.getActiveView() == this) { validateHandles(); for (Handle h : getSelectionHandles()) { h.draw(g); } for (Handle h : getSecondaryHandles()) { h.draw(g); } } } protected void drawTool(Graphics2D g) { if (editor != null && editor.getActiveView() == this && editor.getTool() != null) { editor.getTool().draw(g); } } @Override public void setDrawing(@Nullable Drawing newValue) { Drawing oldValue = drawing; if (this.drawing != null) { this.drawing.removeCompositeFigureListener(eventHandler); this.drawing.removeFigureListener(eventHandler); clearSelection(); } this.drawing = newValue; if (this.drawing != null) { this.drawing.addCompositeFigureListener(eventHandler); this.drawing.addFigureListener(eventHandler); } dirtyArea.add(bufferedArea); firePropertyChange(DRAWING_PROPERTY, oldValue, newValue); // Revalidate without flickering revalidate(); validateViewTranslation(); paintEnabled = false; javax.swing.Timer t = new javax.swing.Timer(10, new ActionListener() { @Override public void actionPerformed(ActionEvent e) { repaint(); paintEnabled = true; } }); t.setRepeats(false); t.start(); } @Override public void paint(Graphics g) { if (paintEnabled) { super.paint(g); } } protected void repaintDrawingArea(Rectangle2D.Double r) { Rectangle vr = drawingToView(r); vr.grow(2, 2); dirtyArea.add(vr); repaint(vr); } @Override public void invalidate() { invalidateDimension(); super.invalidate(); } @Override public void removeNotify() { super.removeNotify(); if (drawingBufferNV != null) { drawingBufferNV.flush(); drawingBufferNV = null; } if (drawingBufferV != null) { drawingBufferV.flush(); drawingBufferV = null; } } /** * Adds a figure to the current selection. */ @Override public void addToSelection(Figure figure) { if (DEBUG) { System.out.println("DefaultDrawingView" + ".addToSelection(" + figure + ")"); } Set<Figure> oldSelection = new HashSet<>(selectedFigures); if (selectedFigures.add(figure)) { figure.addFigureListener(handleInvalidator); Set<Figure> newSelection = new HashSet<>(selectedFigures); Rectangle invalidatedArea = null; if (handlesAreValid && getEditor() != null) { for (Handle h : figure.createHandles(detailLevel)) { h.setView(this); selectionHandles.add(h); h.addHandleListener(eventHandler); if (invalidatedArea == null) { invalidatedArea = h.getDrawingArea(); } else { invalidatedArea.add(h.getDrawingArea()); } } } fireSelectionChanged(oldSelection, newSelection); if (invalidatedArea != null) { repaint(invalidatedArea); } } } /** * Adds a collection of figures to the current selection. */ @Override public void addToSelection(Collection<Figure> figures) { Set<Figure> oldSelection = new HashSet<>(selectedFigures); Set<Figure> newSelection = new HashSet<>(selectedFigures); boolean selectionChanged = false; Rectangle invalidatedArea = null; for (Figure figure : figures) { if (selectedFigures.add(figure)) { selectionChanged = true; newSelection.add(figure); figure.addFigureListener(handleInvalidator); if (handlesAreValid && getEditor() != null) { for (Handle h : figure.createHandles(detailLevel)) { h.setView(this); selectionHandles.add(h); h.addHandleListener(eventHandler); if (invalidatedArea == null) { invalidatedArea = h.getDrawingArea(); } else { invalidatedArea.add(h.getDrawingArea()); } } } } } if (selectionChanged) { fireSelectionChanged(oldSelection, newSelection); if (invalidatedArea != null) { repaint(invalidatedArea); } } } /** * Removes a figure from the selection. */ @Override public void removeFromSelection(Figure figure) { Set<Figure> oldSelection = new HashSet<>(selectedFigures); if (selectedFigures.remove(figure)) { Set<Figure> newSelection = new HashSet<>(selectedFigures); invalidateHandles(); figure.removeFigureListener(handleInvalidator); fireSelectionChanged(oldSelection, newSelection); repaint(); } } /** * If a figure isn't selected it is added to the selection. Otherwise it is removed from the * selection. */ @Override public void toggleSelection(Figure figure) { if (selectedFigures.contains(figure)) { removeFromSelection(figure); } else { addToSelection(figure); } } @Override public void setEnabled(boolean b) { super.setEnabled(b); setCursor(Cursor.getPredefinedCursor(b ? Cursor.DEFAULT_CURSOR : Cursor.WAIT_CURSOR)); } /** * Selects all selectable figures. */ @Override public void selectAll() { Set<Figure> oldSelection = new HashSet<>(selectedFigures); selectedFigures.clear(); for (Figure figure : drawing.getChildren()) { if (figure.isSelectable()) { selectedFigures.add(figure); } } Set<Figure> newSelection = new HashSet<>(selectedFigures); invalidateHandles(); fireSelectionChanged(oldSelection, newSelection); repaint(); } /** * Clears the current selection. */ @Override public void clearSelection() { if (getSelectionCount() > 0) { Set<Figure> oldSelection = new HashSet<>(selectedFigures); selectedFigures.clear(); Set<Figure> newSelection = new HashSet<>(selectedFigures); invalidateHandles(); fireSelectionChanged(oldSelection, newSelection); } } /** * Test whether a given figure is selected. */ @Override public boolean isFigureSelected(Figure checkFigure) { return selectedFigures.contains(checkFigure); } /** * Gets the current selection as a FigureSelection. A FigureSelection can be cut, copied, * pasted. */ @Override public Set<Figure> getSelectedFigures() { return Collections.unmodifiableSet(selectedFigures); } /** * Gets the number of selected figures. */ @Override public int getSelectionCount() { return selectedFigures.size(); } /** * Gets the currently active selection handles. */ private java.util.List<Handle> getSelectionHandles() { validateHandles(); return Collections.unmodifiableList(selectionHandles); } /** * Gets the currently active secondary handles. */ private java.util.List<Handle> getSecondaryHandles() { validateHandles(); return Collections.unmodifiableList(secondaryHandles); } /** * Invalidates the handles. */ private void invalidateHandles() { if (handlesAreValid) { handlesAreValid = false; Rectangle invalidatedArea = null; for (Handle handle : selectionHandles) { handle.removeHandleListener(eventHandler); if (invalidatedArea == null) { invalidatedArea = handle.getDrawingArea(); } else { invalidatedArea.add(handle.getDrawingArea()); } handle.dispose(); } for (Handle handle : secondaryHandles) { handle.removeHandleListener(eventHandler); if (invalidatedArea == null) { invalidatedArea = handle.getDrawingArea(); } else { invalidatedArea.add(handle.getDrawingArea()); } handle.dispose(); } selectionHandles.clear(); secondaryHandles.clear(); setActiveHandle(null); if (invalidatedArea != null) { repaint(invalidatedArea); } } } /** * Validates the handles. */ private void validateHandles() { // Validate handles only, if they are invalid, and if // the DrawingView has a DrawingEditor. if (!handlesAreValid && getEditor() != null) { handlesAreValid = true; selectionHandles.clear(); Rectangle invalidatedArea = null; while (true) { for (Figure figure : getSelectedFigures()) { for (Handle handle : figure.createHandles(detailLevel)) { handle.setView(this); selectionHandles.add(handle); handle.addHandleListener(eventHandler); if (invalidatedArea == null) { invalidatedArea = handle.getDrawingArea(); } else { invalidatedArea.add(handle.getDrawingArea()); } } } if (selectionHandles.size() == 0 && detailLevel != 0) { // No handles are available at the desired detail level. // Retry with detail level 0. detailLevel = 0; continue; } break; } if (invalidatedArea != null) { repaint(invalidatedArea); } } } /** * Finds a handle at a given coordinates. * * @return A handle, null if no handle is found. */ @Override public Handle findHandle( Point p) { validateHandles(); for (Handle handle : new ReversedList<>(getSecondaryHandles())) { if (handle.contains(p)) { return handle; } } for (Handle handle : new ReversedList<>(getSelectionHandles())) { if (handle.contains(p)) { return handle; } } return null; } /** * Gets compatible handles. * * @return A collection containing the handle and all compatible handles. */ @Override public Collection<Handle> getCompatibleHandles(Handle master) { validateHandles(); HashSet<Figure> owners = new HashSet<>(); LinkedList<Handle> compatibleHandles = new LinkedList<>(); owners.add(master.getOwner()); compatibleHandles.add(master); for (Handle handle : getSelectionHandles()) { if (!owners.contains(handle.getOwner()) && handle.isCombinableWith(master)) { owners.add(handle.getOwner()); compatibleHandles.add(handle); } } return compatibleHandles; } /** * Finds a figure at a given coordinates. * * @return A figure, null if no figure is found. */ @Override public Figure findFigure( Point p) { return getDrawing().findFigure(viewToDrawing(p)); } @Override public Collection<Figure> findFigures(Rectangle r) { return getDrawing().findFigures(viewToDrawing(r)); } @Override public Collection<Figure> findFiguresWithin(Rectangle r) { return getDrawing().findFiguresWithin(viewToDrawing(r)); } @Override public void addFigureSelectionListener(FigureSelectionListener fsl) { listenerList.add(FigureSelectionListener.class, fsl); } @Override public void removeFigureSelectionListener(FigureSelectionListener fsl) { listenerList.remove(FigureSelectionListener.class, fsl); } /** * Notify all listenerList that have registered interest for notification on this event type. * Also notify listeners who listen for {@link EditableComponent#SELECTION_EMPTY_PROPERTY}. */ protected void fireSelectionChanged( Set<Figure> oldValue, Set<Figure> newValue) { if (listenerList.getListenerCount() > 0) { FigureSelectionEvent event = null; // Notify all listeners that have registered interest for // Guaranteed to return a non-null array Object[] listeners = listenerList.getListenerList(); // Process the listeners last to first, notifying // those that are interested in this event for (int i = listeners.length - 2; i >= 0; i -= 2) { if (listeners[i] == FigureSelectionListener.class) { // Lazily create the event: if (event == null) { event = new FigureSelectionEvent(this, oldValue, newValue); } ((FigureSelectionListener) listeners[i + 1]).selectionChanged(event); } } } firePropertyChange(EditableComponent.SELECTION_EMPTY_PROPERTY, oldValue.isEmpty(), newValue.isEmpty()); } protected void invalidateDimension() { cachedPreferredSize = null; cachedDrawingArea = null; } @Override public Constrainer getConstrainer() { return isConstrainerVisible() ? visibleConstrainer : invisibleConstrainer; } @Override public Dimension getPreferredSize() { if (cachedPreferredSize == null) { Rectangle2D.Double r = getDrawingArea(); Double cw = getDrawing() == null ? null : getDrawing().get(CANVAS_WIDTH); Double ch = getDrawing() == null ? null : getDrawing().get(CANVAS_HEIGHT); Insets insets = getInsets(); if (cw == null || ch == null) { cachedPreferredSize = new Dimension( (int) Math.ceil((Math.max(0, r.x) + r.width) * scaleFactor) + insets.left + insets.right, (int) Math.ceil((Math.max(0, r.y) + r.height) * scaleFactor) + insets.top + insets.bottom); } else { cachedPreferredSize = new Dimension( (int) Math.ceil((-Math.min(0, r.x) + Math.max(Math.max(0, r.x) + r.width + Math.min(0, r.x), cw)) * scaleFactor) + insets.left + insets.right, (int) Math.ceil((-Math.min(0, r.y) + Math.max(Math.max(0, r.y) + r.height + Math.min(0, r.y), ch)) * scaleFactor) + insets.top + insets.bottom); } } return (Dimension) cachedPreferredSize.clone(); } protected Rectangle2D.Double getDrawingArea() { if (cachedDrawingArea == null) { if (drawing != null) { cachedDrawingArea = drawing.getDrawingArea(1.0); } else { cachedDrawingArea = new Rectangle2D.Double(); } } return (Rectangle2D.Double) cachedDrawingArea.clone(); } /** * Side effect: Changes view Translation. */ @Override public void setBounds(int x, int y, int width, int height) { super.setBounds(x, y, width, height); validateViewTranslation(); } /** * Updates the view translation taking into account the current dimension of the view * JComponent, the size of the drawing, and the scale factor. */ private void validateViewTranslation() { if (getDrawing() == null) { translation.x = translation.y = 0; return; } Point oldTranslation = (Point) translation.clone(); int width = getWidth(); int height = getHeight(); Insets insets = getInsets(); Rectangle2D.Double da = getDrawingArea(); Rectangle r = new Rectangle((int) (da.x * scaleFactor), (int) (da.y * scaleFactor), (int) (da.width * scaleFactor), (int) (da.height * scaleFactor)); Double cwd = getDrawing().get(CANVAS_WIDTH); Double chd = getDrawing().get(CANVAS_HEIGHT); if (cwd == null || chd == null) { // The canvas size is not explicitly specified. //Place the canvas at the top left translation.x = insets.top; translation.y = insets.left; } else { // The canvas size is explicitly specified. int cw, ch; cw = (int) (cwd * scaleFactor); ch = (int) (chd * scaleFactor); //Place the canvas at the center if (cw < width) { translation.x = insets.left + (width - insets.left - insets.right - cw) / -2; } if (ch < height) { translation.y = insets.top + (height - insets.top - insets.bottom - ch) / -2; } } if (r.y + r.height - translation.y > (height - insets.bottom)) { // We cut off the lower part of the drawing -> shift the canvas up translation.y = r.y + r.height - (height - insets.bottom); } if (Math.min(0, r.y) - translation.y < insets.top) { // We cut off the upper part of the drawing -> shift the canvas down translation.y = Math.min(0, r.y) - insets.top; } if (r.x + r.width - translation.x > (width - insets.right)) { // We cut off the right part of the drawing -> shift the canvas left translation.x = r.x + r.width - (width - insets.right); } if (Math.min(0, r.x) - translation.x < insets.left) { // We cut off the left part of the drawing -> shift the canvas right translation.x = Math.min(0, r.x) - insets.left; } if (!oldTranslation.equals(translation)) { bufferedArea.translate(oldTranslation.x - translation.x, oldTranslation.y - translation.y); fireViewTransformChanged(); } } /** * Converts drawing coordinates to view coordinates. */ @Override public Point drawingToView( Point2D.Double p) { return new Point( (int) (p.x * scaleFactor) - translation.x, (int) (p.y * scaleFactor) - translation.y); } @Override public Rectangle drawingToView( Rectangle2D.Double r) { return new Rectangle( (int) (r.x * scaleFactor) - translation.x, (int) (r.y * scaleFactor) - translation.y, (int) (r.width * scaleFactor), (int) (r.height * scaleFactor)); } /** * Converts view coordinates to drawing coordinates. */ @Override public Point2D.Double viewToDrawing(Point p) { return new Point2D.Double( (p.x + translation.x) / scaleFactor, (p.y + translation.y) / scaleFactor); } @Override public Rectangle2D.Double viewToDrawing(Rectangle r) { return new Rectangle2D.Double( (r.x + translation.x) / scaleFactor, (r.y + translation.y) / scaleFactor, r.width / scaleFactor, r.height / scaleFactor); } @Override public JComponent getComponent() { return this; } @Override public double getScaleFactor() { return scaleFactor; } @Override public void setScaleFactor(double newValue) { double oldValue = scaleFactor; scaleFactor = newValue; validateViewTranslation(); dirtyArea.setBounds(bufferedArea); invalidateHandles(); revalidate(); repaint(); firePropertyChange("scaleFactor", oldValue, newValue); } protected void fireViewTransformChanged() { for (Handle handle : selectionHandles) { handle.viewTransformChanged(); } for (Handle handle : secondaryHandles) { handle.viewTransformChanged(); } } @Override public void setHandleDetailLevel(int newValue) { if (newValue != detailLevel) { detailLevel = newValue; invalidateHandles(); validateHandles(); } } @Override public int getHandleDetailLevel() { return detailLevel; } @Override public AffineTransform getDrawingToViewTransform() { AffineTransform t = new AffineTransform(); t.translate(-translation.x, -translation.y); t.scale(scaleFactor, scaleFactor); return t; } @Override public void delete() { final java.util.List<Figure> deletedFigures = drawing.sort(getSelectedFigures()); // Abort, if not all of the selected figures may be removed from the // drawing for (Figure f : deletedFigures) { if (!f.isRemovable()) { getToolkit().beep(); return; } } // Get z-indices of deleted figures final int[] deletedFigureIndices = new int[deletedFigures.size()]; for (int i = 0; i < deletedFigureIndices.length; i++) { deletedFigureIndices[i] = drawing.indexOf(deletedFigures.get(i)); } clearSelection(); getDrawing().removeAll(deletedFigures); getDrawing().fireUndoableEditHappened(new AbstractUndoableEdit() { private static final long serialVersionUID = 1L; @Override public String getPresentationName() { ResourceBundleUtil labels = ResourceBundleUtil.getBundle("org.jhotdraw.draw.Labels"); return labels.getString("edit.delete.text"); } @Override public void undo() throws CannotUndoException { super.undo(); clearSelection(); Drawing d = getDrawing(); for (int i = 0; i < deletedFigureIndices.length; i++) { d.add(deletedFigureIndices[i], deletedFigures.get(i)); } addToSelection(deletedFigures); } @Override public void redo() throws CannotRedoException { super.redo(); for (int i = 0; i < deletedFigureIndices.length; i++) { drawing.remove(deletedFigures.get(i)); } } }); } @Override public void duplicate() { Collection<Figure> sorted = getDrawing().sort(getSelectedFigures()); HashMap<Figure, Figure> originalToDuplicateMap = new HashMap<>(sorted.size()); clearSelection(); final ArrayList<Figure> duplicates = new ArrayList<>(sorted.size()); AffineTransform tx = new AffineTransform(); tx.translate(5, 5); for (Figure f : sorted) { Figure d = f.clone(); d.transform(tx); duplicates.add(d); originalToDuplicateMap.put(f, d); drawing.add(d); } for (Figure f : duplicates) { f.remap(originalToDuplicateMap, false); } addToSelection(duplicates); getDrawing().fireUndoableEditHappened(new AbstractUndoableEdit() { private static final long serialVersionUID = 1L; @Override public String getPresentationName() { ResourceBundleUtil labels = ResourceBundleUtil.getBundle("org.jhotdraw.draw.Labels"); return labels.getString("edit.duplicate.text"); } @Override public void undo() throws CannotUndoException { super.undo(); getDrawing().removeAll(duplicates); } @Override public void redo() throws CannotRedoException { super.redo(); getDrawing().addAll(duplicates); } }); } @Override public void removeNotify(DrawingEditor editor) { this.editor = null; repaint(); } @Override public void addNotify(DrawingEditor editor) { DrawingEditor oldValue = editor; this.editor = editor; firePropertyChange("editor", oldValue, editor); invalidateHandles(); repaint(); } @Override public void setVisibleConstrainer(Constrainer newValue) { Constrainer oldValue = visibleConstrainer; visibleConstrainer = newValue; firePropertyChange(VISIBLE_CONSTRAINER_PROPERTY, oldValue, newValue); } @Override public Constrainer getVisibleConstrainer() { return visibleConstrainer; } @Override public void setInvisibleConstrainer(Constrainer newValue) { Constrainer oldValue = invisibleConstrainer; invisibleConstrainer = newValue; firePropertyChange(INVISIBLE_CONSTRAINER_PROPERTY, oldValue, newValue); } @Override public Constrainer getInvisibleConstrainer() { return invisibleConstrainer; } @Override public void setConstrainerVisible(boolean newValue) { boolean oldValue = isConstrainerVisible; isConstrainerVisible = newValue; firePropertyChange(CONSTRAINER_VISIBLE_PROPERTY, oldValue, newValue); repaint(); } @Override public boolean isConstrainerVisible() { return isConstrainerVisible; } /** * Sets whether the drawing is double buffered. * <p> * The default value is true. * <p> * This is a bound property. * <p> * If the drawing view is used for editing, you should leave this to true. If the drawing view * is used for viewing only, you should set this to false. */ public void setDrawingDoubleBuffered(boolean newValue) { boolean oldValue = isDrawingDoubleBuffered; isDrawingDoubleBuffered = newValue; if (!isDrawingDoubleBuffered && drawingBufferV != null) { drawingBufferV.flush(); drawingBufferV = null; } if (!isDrawingDoubleBuffered && drawingBufferNV != null) { drawingBufferNV.flush(); drawingBufferNV = null; } firePropertyChange(DRAWING_DOUBLE_BUFFERED_PROPERTY, oldValue, newValue); } /** * Returns true, if the the drawing is double buffered. */ public boolean isDrawingDoubleBuffered() { return isDrawingDoubleBuffered; } /** * Returns a paint for drawing the background of the drawing area. * * @return Paint. */ protected Paint getBackgroundPaint( int x, int y) { if (backgroundTile == null) { backgroundTile = new BufferedImage(16, 16, BufferedImage.TYPE_INT_RGB); Graphics2D g = backgroundTile.createGraphics(); g.setColor(Color.white); g.fillRect(0, 0, 16, 16); g.setColor(new Color(0xdfdfdf)); g.fillRect(0, 0, 8, 8); g.fillRect(8, 8, 8, 8); g.dispose(); } return new TexturePaint(backgroundTile, new Rectangle(x, y, backgroundTile.getWidth(), backgroundTile.getHeight())); } @Override public DrawingEditor getEditor() { return editor; } // Variables declaration - do not modify//GEN-BEGIN:variables // End of variables declaration//GEN-END:variables @Override public void setActiveHandle(@Nullable Handle newValue) { Handle oldValue = activeHandle; if (oldValue != null) { repaint(oldValue.getDrawingArea()); } activeHandle = newValue; if (newValue != null) { repaint(newValue.getDrawingArea()); } firePropertyChange(ACTIVE_HANDLE_PROPERTY, oldValue, newValue); } @Override public Handle getActiveHandle() { return activeHandle; } }