/* * JFlow * Created by Tim De Pauw <http://pwnt.be/> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package be.pwnt.jflow; import java.awt.*; import java.awt.event.*; import java.util.Collection; import java.util.HashSet; import java.util.Timer; import java.util.TimerTask; import javax.swing.JPanel; import javax.swing.SwingUtilities; import javax.swing.event.ListDataEvent; import javax.swing.event.ListDataListener; import be.pwnt.jflow.event.ShapeEvent; import be.pwnt.jflow.event.ShapeListener; import be.pwnt.jflow.geometry.Point3D; import be.pwnt.jflow.geometry.RotationMatrix; import be.pwnt.jflow.model.FlowModel; import be.pwnt.jflow.shape.Picture; @SuppressWarnings("serial") public class JFlowPanel extends JPanel { private Collection<ShapeListener> listeners; private Configuration config; private FlowModel flowModel; private Scene scene; private double scrollDelta; private double dragStart; private double dragRate; private boolean buttonOnePressed; private boolean dragging; private Shape activeShape; private Shape centerShape; private Timer autoScrollToCenterTimer; private Timer commandRollerTimer; private int shapeArrayOffset; private EventAdapter ea; public JFlowPanel(Configuration config, FlowModel flowModel) { super(); this.config = config; this.flowModel = flowModel; ea = new EventAdapter(); flowModel.addListDataListener(ea); listeners = new HashSet<ShapeListener>(); scene = new Scene(new Point3D(0, 0, 1), new RotationMatrix(0, 0, 0), new Point3D(0, 0, 1)); buttonOnePressed = false; dragging = false; shapeArrayOffset = 0; activeShape = null; centerShape = null; setLayout(null); setBackground(config.backgroundColor); setScrollRate(0); if (config.autoScrollAmount != 0) { new Timer().scheduleAtFixedRate(new AutoScroller(), 0, 1000 / config.framesPerSecond); } addMouseListener(ea); addMouseMotionListener(ea); //to make it work with touch pad of macbook. addMouseWheelListener(ea); } public void addShapeListener(ShapeListener listener) { listeners.add(listener); } public void removeShapeListener(ShapeListener listener) { listeners.remove(listener); } public synchronized double getScrollRate() { return scrollDelta; } public synchronized void setScrollRate(double scrollRate) { // System.out.println("scrollRate = " + scrollRate); this.scrollDelta = scrollRate; normalizeScrollRate(); updateShapes(); if (scrollDelta == 0) { Shape shape = flowModel.getShape(getShapeIndexByFlowPos(config.visibleShapeCount / 2)); setCenterShape(shape); } } private synchronized void normalizeScrollRate() { while (scrollDelta < -0.5) { scrollDelta += 1; if (--shapeArrayOffset < 0) { shapeArrayOffset += flowModel.getSize(); } } while (scrollDelta > 0.5) { scrollDelta -= 1; if (++shapeArrayOffset >= flowModel.getSize()) { shapeArrayOffset -= flowModel.getSize(); } } } private void updateShape(int i, double maxHeight) { int shapeIndex = getShapeIndexByFlowPos(i); if(flowModel.getShape(shapeIndex) instanceof Picture) { Picture pic = (Picture)flowModel.getShape(shapeIndex); double j = i - config.visibleShapeCount / 2 + scrollDelta; j = (j < 0 ? -1 : 1) * Math.pow(Math.abs(j), config.scrollScale); double comp = 0; if(j < 0) { comp = config.shapeWidth / 2; } else if(j > 0) { comp = -config.shapeWidth / 2; } double height = config.shapeWidth * pic.getHeight() / pic.getWidth(); double top, bottom; switch(config.verticalShapeAlignment) { case TOP: top = maxHeight / 2 - height; bottom = maxHeight / 2; break; case BOTTOM: top = -maxHeight / 2; bottom = -maxHeight / 2 + height; break; default: top = -height / 2; bottom = height / 2; } double z = -config.zoomFactor * Math.pow(Math.abs(j), config.zoomScale); Point3D topLeft = new Point3D( -config.shapeWidth / 2 + (config.shapeOrientation == ComponentOrientation.LEFT_TO_RIGHT ? -1 : 1) * comp, top, 0); Point3D bottomRight = new Point3D( config.shapeWidth / 2 + (config.shapeOrientation == ComponentOrientation.LEFT_TO_RIGHT ? -1 : 1) * comp, bottom, 0); pic.setCoordinates(topLeft, bottomRight); pic.setRotationMatrix(new RotationMatrix(0, (config.shapeOrientation == ComponentOrientation.LEFT_TO_RIGHT ? 1 : -1) * config.shapeRotation * j, 0)); pic.setLocation(new Point3D( (config.shapeOrientation == ComponentOrientation.LEFT_TO_RIGHT ? 1 : -1) * (-config.shapeSpacing * j + comp), 0, z)); } } // FIXME only works for Pictures private synchronized void updateShapes() { double maxHeight = 0; for (int i = 0; i < config.visibleShapeCount; i++) { Shape shape = flowModel.getShape(getShapeIndexByFlowPos(i)); if (shape instanceof Picture) { Picture pic = (Picture) shape; double height = config.shapeWidth * pic.getHeight() / pic.getWidth(); if (height > maxHeight) { maxHeight = height; } } } //update the shape in the side, then update the middle shape, //so that if the shape count is smaller than visible count, the shapes will align to center. for (int i = 0; i < config.visibleShapeCount / 2; i++) { updateShape(i, maxHeight); updateShape(config.visibleShapeCount - 1 - i, maxHeight); } updateShape(config.visibleShapeCount / 2, maxHeight); checkActiveShape(); repaint(); } public Shape getCenterShape() { return centerShape; } public Shape getActiveShape() { return activeShape; } private synchronized void setCenterShape(Shape shape) { if (centerShape != shape) { centerShape = shape; ShapeEvent evt = new ShapeEvent(shape); for (ShapeListener listener : listeners) { listener.shapeCentered(evt); } } } private synchronized void setActiveShape(Shape shape) { if (activeShape != shape) { if (activeShape != null) { ShapeEvent evt = new ShapeEvent(shape); for (ShapeListener listener : listeners) { listener.shapeDeactivated(evt); } } activeShape = shape; if (activeShape != null) { ShapeEvent evt = new ShapeEvent(shape); for (ShapeListener listener : listeners) { listener.shapeActivated(evt); } } } } @Override public synchronized void paintComponent(Graphics g) { super.paintComponent(g); // respect stacking order for (int i = 0; i < config.visibleShapeCount / 2; i++) { paintShape(flowModel.getShape(getShapeIndexByFlowPos(i)), g); paintShape(flowModel.getShape(getShapeIndexByFlowPos(config.visibleShapeCount - 1 - i)), g); } paintShape(flowModel.getShape(getShapeIndexByFlowPos(config.visibleShapeCount / 2)), g); } // get shape index by ui position private int getShapeIndexByFlowPos(int flowPosition) { // System.out.println("ui position = " + flowPosition); int size = flowModel.getSize(); if (size <= 0) return -1; int pos = (flowPosition - shapeArrayOffset) % size; while (pos < 0) { pos += size; } while (pos >= size) { pos -= size; } // System.out.println("shape index = " + pos); return pos; } // get ui position by shape index private int getFlowPosByShapeIndex(int shapeIndex) { // System.out.println("shape index = " + shapeIndex); int size = flowModel.getSize(); if (size <= 0) return -1; int pos = (shapeIndex + shapeArrayOffset) % size; while (pos < 0) { pos += size; } while (pos >= size) { pos -= size; } // System.out.println("ui pos = " + pos); //the shape is not in the UI, so return -1; if (pos < 0 || pos >= config.visibleShapeCount) return -1; return pos; } private void paintShape(Shape shape, Graphics g) { shape.paint(g, scene, getSize(), shape == activeShape, config); } private void checkActiveShape() { if (config.enableShapeSelection) { SwingUtilities.invokeLater(new ActiveShapeChecker()); } } private void updateCursor() { setCursor(dragging ? Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR) : (activeShape == null ? Cursor.getDefaultCursor() : Cursor .getPredefinedCursor(Cursor.HAND_CURSOR))); } public void scrollToShape(Shape newShape) { int newShapeIndex = flowModel.getShapeIndex(newShape); int newShapeUiPos = getFlowPosByShapeIndex(newShapeIndex); if (newShapeUiPos == (config.visibleShapeCount / 2)) return; if(commandRollerTimer != null) { commandRollerTimer.cancel(); } if(autoScrollToCenterTimer != null)//cancel the easing timer, otherwise easing timer will mess the command roller timer. { autoScrollToCenterTimer.cancel(); } commandRollerTimer = new Timer(); double scrollOffset = (config.visibleShapeCount / 2) - newShapeUiPos + scrollDelta; commandRollerTimer.scheduleAtFixedRate(new CommandScroller(scrollOffset), 0, (long)(config.commandRollerInterval * 1000)); } private class AutoScroller extends TimerTask { @Override public void run() { if (!dragging) { // System.out.println("Auto Scroller"); setScrollRate(getScrollRate() + config.autoScrollAmount); } } } private class CommandScroller extends TimerTask { private final double offsetPer; private final long taskCount; private long count; public CommandScroller(double scrollOffset) { taskCount = Math.round(config.commandRollerDuration / config.commandRollerInterval); offsetPer = scrollOffset / taskCount; count = 0; } @Override public void run() { if (offsetPer == 0 || count > taskCount) { commandRollerTimer.cancel(); setScrollRate(0);//so that the shape will always be in the center position. return; } // System.out.println("Command Scroller"); setScrollRate(scrollDelta + offsetPer); count ++; } } private class AutoScrollToCenterTask extends TimerTask { @Override public void run() { if (scrollDelta > 0) { if (scrollDelta > config.dragEaseOutFactor) scrollDelta -= config.dragEaseOutFactor; else scrollDelta = 0; } else { if (-scrollDelta > config.dragEaseOutFactor) scrollDelta += config.dragEaseOutFactor; else scrollDelta = 0; } // System.out.println("Eraser Scroller"); setScrollRate(scrollDelta); if (scrollDelta == 0) autoScrollToCenterTimer.cancel(); } } private class ActiveShapeChecker implements Runnable { @Override public void run() { if (!dragging) { Point mp = getMousePosition(); Shape newActiveShape = null; if (mp != null) { Point3D p = new Point3D(mp.getX(), mp.getY(), 0); int i = getShapeIndexByFlowPos(config.visibleShapeCount / 2); if (flowModel.getShape(i).contains(p)) { newActiveShape = flowModel.getShape(i); } i = 1; while (i <= config.visibleShapeCount / 2 && newActiveShape == null) { int j = getShapeIndexByFlowPos(config.visibleShapeCount / 2 - i); int k = getShapeIndexByFlowPos(config.visibleShapeCount / 2 + i); if (flowModel.getShape(j).contains(p)) { newActiveShape = flowModel.getShape(j); } else if (flowModel.getShape(k).contains(p)) { newActiveShape = flowModel.getShape(k); } i++; } } repaint(); if (activeShape != newActiveShape) { setActiveShape(newActiveShape); } } updateCursor(); } } private class EventAdapter implements MouseListener, MouseMotionListener, MouseWheelListener, ListDataListener { @Override public void mouseClicked(MouseEvent e) { if(config.enableShapeSelection && activeShape != null) { ShapeEvent evt = new ShapeEvent(activeShape, e); for(ShapeListener listener : listeners) { listener.shapeClicked(evt); } } } @Override public void mouseEntered(MouseEvent e) { } @Override public void mouseExited(MouseEvent e) { } @Override public void mousePressed(MouseEvent e) { if(config.autoScrollAmount == 0) { if(autoScrollToCenterTimer != null) { autoScrollToCenterTimer.cancel(); } } if(e.getButton() == MouseEvent.BUTTON1) { buttonOnePressed = true; dragStart = e.getLocationOnScreen().getX(); } } @Override public void mouseReleased(MouseEvent e) { buttonOnePressed = false; if(e.getButton() == MouseEvent.BUTTON1) { dragging = false; updateCursor(); checkActiveShape(); if(config.autoScrollAmount == 0 && config.autoCentralizeShape) { if(autoScrollToCenterTimer != null) { autoScrollToCenterTimer.cancel(); } autoScrollToCenterTimer = new Timer(); autoScrollToCenterTimer.scheduleAtFixedRate(new AutoScrollToCenterTask(), 0, 100); } } } @Override public void mouseDragged(MouseEvent e) { if(config.autoScrollAmount == 0) { if(autoScrollToCenterTimer != null) { autoScrollToCenterTimer.cancel(); } } if(buttonOnePressed) { dragging = true; setActiveShape(null); updateCursor(); double dragEnd = e.getLocationOnScreen().getX(); dragRate = config.scrollFactor * (dragEnd - dragStart) / getWidth(); setScrollRate(getScrollRate() + (config.inverseScrolling ? dragRate : -dragRate)); dragStart = dragEnd; } } @Override public void mouseMoved(MouseEvent e) { checkActiveShape(); } @Override public void mouseWheelMoved(MouseWheelEvent e) { dragRate = config.scrollFactor * e.getUnitsToScroll() / getWidth(); setScrollRate(getScrollRate() + (config.inverseScrolling ? dragRate : -dragRate)); checkActiveShape(); if(config.autoScrollAmount == 0 && config.autoCentralizeShape) { if(autoScrollToCenterTimer != null) { autoScrollToCenterTimer.cancel(); } autoScrollToCenterTimer = new Timer(); autoScrollToCenterTimer.scheduleAtFixedRate(new AutoScrollToCenterTask(), 0, 100); } } @Override public void intervalAdded(ListDataEvent e) { System.out.println("JFlowPanel$EventAdapter.intervalAdded"); updateShapes(); } @Override public void intervalRemoved(ListDataEvent e) { System.out.println("JFlowPanel$EventAdapter.intervalRemoved"); updateShapes(); } @Override public void contentsChanged(ListDataEvent e) { System.out.println("JFlowPanel$EventAdapter.contentsChanged"); updateShapes(); } } }