/* * Copyright (C) 2015 JHotDraw. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301 USA */ package org.jhotdraw.draw; import static com.sun.java.accessibility.util.AWTEventMonitor.addFocusListener; import java.awt.Color; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Paint; import java.awt.Point; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.Shape; import java.awt.TexturePaint; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.MouseEvent; import java.awt.geom.AffineTransform; import java.awt.geom.NoninvertibleTransformException; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.Nullable; import javax.swing.JLabel; import javax.swing.event.EventListenerList; import javax.swing.undo.AbstractUndoableEdit; import javax.swing.undo.CannotRedoException; import javax.swing.undo.CannotUndoException; import static org.jhotdraw.draw.AttributeKeys.CANVAS_FILL_COLOR; import static org.jhotdraw.draw.AttributeKeys.CANVAS_FILL_OPACITY; import static org.jhotdraw.draw.AttributeKeys.CANVAS_HEIGHT; import static org.jhotdraw.draw.AttributeKeys.CANVAS_WIDTH; import static org.jhotdraw.draw.DrawingView.ACTIVE_HANDLE_PROPERTY; import static org.jhotdraw.draw.DrawingView.CONSTRAINER_VISIBLE_PROPERTY; import static org.jhotdraw.draw.DrawingView.DRAWING_PROPERTY; import static org.jhotdraw.draw.DrawingView.INVISIBLE_CONSTRAINER_PROPERTY; import static org.jhotdraw.draw.DrawingView.VISIBLE_CONSTRAINER_PROPERTY; import org.jhotdraw.draw.event.CompositeFigureEvent; import org.jhotdraw.draw.event.CompositeFigureListener; import org.jhotdraw.draw.event.FigureAdapter; import org.jhotdraw.draw.event.FigureEvent; import org.jhotdraw.draw.event.FigureListener; import org.jhotdraw.draw.event.FigureSelectionEvent; import org.jhotdraw.draw.event.FigureSelectionListener; import org.jhotdraw.draw.event.HandleEvent; import org.jhotdraw.draw.event.HandleListener; import org.jhotdraw.draw.handle.Handle; import org.jhotdraw.gui.EditableComponent; import org.jhotdraw.util.ResourceBundleUtil; import org.jhotdraw.util.ReversedList; /** * Implementation of DrawingView using no JComponent as a backend. The displaying container comes * with a delegator class. Therefore we have a Swing independend implementaion. * * @author tw */ public abstract class AbstractDrawingView implements DrawingView, EditableComponent { private static final Logger LOG = Logger.getLogger(AbstractDrawingView.class.getName()); @Nullable private Drawing drawing; private transient final PropertyChangeSupport propertyChangeSupport = new PropertyChangeSupport(this); private transient final EventListenerList listenerList = new EventListenerList(); /** * Holds the selected figures in an ordered put. The ordering reflects the sequence that was * used to select the figures. */ private final Set<Figure> selectedFigures = new LinkedHashSet<>(); private final List<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 final List<Handle> secondaryHandles = new LinkedList<>(); private boolean handlesAreValid = true; //TODO replace with AffineTransform to support rotation and more complex transformations from //document to view //private double scaleFactor = 1; //private Point translation = new Point(0, 0); private int detailLevel; @Nullable private DrawingEditor editor; private JLabel emptyDrawingLabel; private boolean paintBackground = true; protected BufferedImage backgroundTile; private final FigureListener handleInvalidator = new FigureAdapter() { @Override public void figureHandlesChanged(FigureEvent e) { invalidateHandles(); } }; public boolean isPaintBackground() { return paintBackground; } public void setPaintBackground(boolean paintBackground) { this.paintBackground = paintBackground; } private boolean paintEnabled = true; @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.getCompositeFigure().getDrawingArea(AttributeKeys.getScaleFactor(getDrawingToViewTransform()))); } } @Override public void figureRemoved(CompositeFigureEvent evt) { if (drawing.getChildCount() == 0 && getEmptyDrawingMessage() != null) { repaint(); } else { repaintDrawingArea(evt.getCompositeFigure().getDrawingArea(AttributeKeys.getScaleFactor(getDrawingToViewTransform()))); } removeFromSelection(evt.getChildFigure()); } @Override public void areaInvalidated(FigureEvent evt) { repaintDrawingArea(evt.getFigure().getDrawingArea(AttributeKeys.getScaleFactor(getDrawingToViewTransform()))); } @Override public void areaInvalidated(HandleEvent evt) { repaint(evt.getInvalidatedArea()); } @Override public void handleRequestSecondaryHandles(HandleEvent e) { secondaryHandleOwner = e.getHandle(); secondaryHandles.clear(); secondaryHandles.addAll(secondaryHandleOwner.createSecondaryHandles()); for (Handle h : secondaryHandles) { h.setView(AbstractDrawingView.this); h.addHandleListener(eventHandler); } repaint(); } @Override public void focusGained(FocusEvent e) { if (editor != null) { editor.setActiveView(AbstractDrawingView.this); } } @Override public void focusLost(FocusEvent e) { } @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)) { repaint(); } if (e.getInvalidatedArea() != null) { repaintDrawingArea(e.getFigure().getDrawingArea(AttributeKeys.getScaleFactor(getDrawingToViewTransform()))); } else { repaintDrawingArea(viewToDrawing(getCanvasViewBounds())); } } else { if (e.getInvalidatedArea() != null) { repaintDrawingArea(e.getFigure().getDrawingArea(AttributeKeys.getScaleFactor(getDrawingToViewTransform()))); } } } @Override public void figureHandlesChanged(FigureEvent e) { } @Override public void figureChanged(FigureEvent e) { repaintDrawingArea(e.getFigure().getDrawingArea(AttributeKeys.getScaleFactor(getDrawingToViewTransform()))); } @Override public void figureAdded(FigureEvent e) { } @Override public void figureRemoved(FigureEvent e) { } @Override public void figureRequestRemove(FigureEvent e) { } } private final EventHandler eventHandler = new EventHandler(); public AbstractDrawingView() { addFocusListener(eventHandler); } @Override @Nullable public Drawing getDrawing() { return drawing; } 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. TODO: Rename to reflect this is not a * Swing or AWT method. */ public void paintComponent(Graphics gr) { if (!isPaintEnabled()) { return; } Graphics2D g = (Graphics2D) gr; setViewRenderingHints(g); if (isPaintBackground()) { drawBackground(g); } drawCanvas(g); drawConstrainer(g); drawDrawing(g); drawHandles(g); drawTool(g); } /** * 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. TODO: rename to * reflect that this is not swing or awt */ public void printComponent(Graphics gr) { if (!isPaintEnabled()) { return; } 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 = 0; int y = 0; 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(); 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(); if (getDrawingToViewTransform() != null) { tx.concatenate(getDrawingToViewTransform()); } 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); } firePropertyChange(DRAWING_PROPERTY, oldValue, newValue); // Revalidate without flickering revalidate(); 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(); } protected void repaintDrawingArea(Rectangle2D.Double r) { Rectangle vr = drawingToView(r); vr.grow(2, 2); repaint(vr); } /** * Adds a figure to the current selection. */ @Override public void addToSelection(Figure 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); } } /** * 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 List<Handle> getSelectionHandles() { validateHandles(); return Collections.unmodifiableList(selectionHandles); } /** * Gets the currently active secondary handles. */ private 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.isEmpty() && 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 drawing == null ? null : drawing.findFigure(viewToDrawing(p)); } @Override public Collection<Figure> findFigures(Rectangle r) { return drawing == null ? Collections.EMPTY_LIST : drawing.findFigures(viewToDrawing(r)); } @Override public Collection<Figure> findFiguresWithin(Rectangle r) { return drawing == null ? Collections.EMPTY_LIST : drawing.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()); } @Override public Constrainer getConstrainer() { return isConstrainerVisible() ? visibleConstrainer : invisibleConstrainer; } protected Rectangle2D.Double getDrawingArea() { return (Rectangle2D.Double) drawing.getDrawingArea().clone(); } /** * Converts drawing coordinates to view coordinates. */ @Override public Point drawingToView(Point2D.Double p) { AffineTransform transform = getDrawingToViewTransform(); Point2D pnt = transform.transform(p, null); return new Point((int) pnt.getX(), (int) pnt.getY()); } /** * Minimal rectangle that encloses drawing rectangle (could be rotated). * * @param r * @return */ @Override public Rectangle drawingToView(Rectangle2D.Double r) { Point pnt = drawingToView(new Point2D.Double(r.getMinX(), r.getMinY())); Rectangle rect = new Rectangle(pnt.x, pnt.y, 1, 1); rect.add(drawingToView(new Point2D.Double(r.getMaxX(), r.getMinY()))); rect.add(drawingToView(new Point2D.Double(r.getMaxX(), r.getMaxY()))); rect.add(drawingToView(new Point2D.Double(r.getMinX(), r.getMaxY()))); return rect; } /** * Converts view coordinates to drawing coordinates. */ @Override public Point2D.Double viewToDrawing(Point p) { try { AffineTransform transform = getDrawingToViewTransform().createInverse(); Point2D.Double pint = new Point2D.Double(); transform.transform(p, pint); return pint; } catch (NoninvertibleTransformException ex) { Logger.getLogger(AbstractDrawingView.class.getName()).log(Level.SEVERE, null, ex); } return null; } /** * Rectangles are not rotated. Therefore we deliver the smallest rectangle that encloses the * rotated target rectangle. */ @Override public Rectangle2D.Double viewToDrawing(Rectangle r) { Point2D.Double pnt = viewToDrawing(new Point(r.x, r.y)); Rectangle2D.Double rect = new Rectangle2D.Double(pnt.x, pnt.y, 1, 1); rect.add(viewToDrawing(new Point((int) r.getMaxX(), (int) r.getMinY()))); rect.add(viewToDrawing(new Point((int) r.getMaxX(), (int) r.getMaxY()))); rect.add(viewToDrawing(new Point((int) r.getMinX(), (int) r.getMaxY()))); return rect; } public 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 abstract AffineTransform getDrawingToViewTransform(); @Override public void delete() { final List<Figure> deletedFigures = drawing.sort(getSelectedFigures()); for (Figure f : deletedFigures) { if (!f.isRemovable()) { 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(); drawing.removeAll(deletedFigures); drawing.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 = drawing; 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 = drawing.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); drawing.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(); drawing.removeAll(duplicates); } @Override public void redo() throws CannotRedoException { super.redo(); drawing.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; } /** * 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; } @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); } private void firePropertyChange(String propertyName, Object oldValue, Object newValue) { propertyChangeSupport.firePropertyChange(propertyName, oldValue, newValue); } @Override public Handle getActiveHandle() { return activeHandle; } public boolean isPaintEnabled() { return paintEnabled; } @Override public void addPropertyChangeListener(PropertyChangeListener listener) { propertyChangeSupport.addPropertyChangeListener(listener); } @Override public void removePropertyChangeListener(PropertyChangeListener listener) { propertyChangeSupport.removePropertyChangeListener(listener); } public abstract void repaint(Rectangle r); public abstract Color getBackground(); public abstract void repaint(); public abstract int getWidth(); public abstract int getHeight(); public abstract void revalidate(); }