/* * CurvePanel.java * (FScape) * * Copyright (c) 2001-2016 Hanns Holger Rutz. All rights reserved. * * This software is published under the GNU General Public License v3+ * * * For further information, please contact Hanns Holger Rutz at * contact@sciss.de * * * Changelog: * 21-May-05 improved ; extends JPanel instead of Panel */ package de.sciss.fscape.gui; import de.sciss.fscape.util.Constants; import de.sciss.fscape.util.Curve; import de.sciss.fscape.util.DoublePoint; import de.sciss.fscape.util.ParamSpace; import javax.swing.*; import javax.swing.event.MouseInputAdapter; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.event.MouseEvent; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.Vector; /** * Panel which hosts breakpoint * editors for parameter dynamization. */ public class CurvePanel extends JPanel { // -------- public variables -------- /** * ActionCommands, die ein registrierter (AddActionListener()) * ActionListener empfaengt. Kann mit == verglichen werden, * equals() nicht noetig */ public static final String ACTION_POINTSELECTED = "PointSelect"; public static final String ACTION_POINTDESELECTED = "PointDeselect"; public static final String ACTION_POINTCHANGED = "PointMoved"; public static final String ACTION_SPACECHANGED = "SpaceChange"; // -------- private variables -------- // gerade angewaehlter Punkt; wenn keiner ausgewaehlt, muss // er auf dummyPoint gesetzt werden, damit keine Null-Pointer Fehler // auftreten koennen! protected CurvePoint currentCurvePoint; protected CurvePoint dummyPoint; protected Cursor lastCursor; // Cursor previously used to dnd or edit-op // this is all for drag+drop protected CurvePoint dragSource = null; protected DoublePoint dragLeft, dragRight; // Punkt links + rechts neben dem gezogenen protected boolean dragState = false; // true for drag+drop protected int dragType = DRAG_NONE; protected int dragLastX; // last coordinates protected int dragLastY; // last coordinates protected DoublePoint dragPoint; private static final int DRAG_MOVE = 0; // drag object private static final int DRAG_NONE = -1; // fuer CurvePoint protected static final int CP_WIDTH = 8; protected static final int CP_HEIGHT = 8; protected static final int CP_STATE_NORMAL = 0; protected static final int CP_STATE_SELECTED = 3; protected static final int CP_STATE_UNKNOWN = -1; protected Curve curve; protected Dimension dim; // Panel-Size; vom Component-Listener geupdated // dieser Button wird nicht angezeigt, sondern dient nur als // Institution zum Verwalten der ActionListener und fuer // den Event-Dispatch! protected Button actionComponent; private final MouseInputAdapter mia; private final boolean isDark = UIManager.getBoolean("dark-skin"); private static Paint pntGridLight = Color.lightGray; private static Paint pntGridDark = Color.darkGray; private static Paint pntGridLightD = new Color(192, 192, 192, 0x7F); private static Paint pntGridDarkD = new Color( 64, 64, 64, 0x7F); private static Paint pntLineLight = Color.black; private static Paint pntLineDark = new Color(200, 200, 200); private static Paint pntLineLightD = new Color( 0, 0, 0, 0x7F); private static Paint pntLineDarkD = new Color(200, 200, 200, 0x7F); // -------- public methods -------- public CurvePanel() { super(null); actionComponent = new Button(); dummyPoint = new CurvePoint(); currentCurvePoint = dummyPoint; curve = new Curve( Constants.spaces[ Constants.emptySpace ], Constants.spaces[ Constants.emptySpace ]); // setBackground(Color.white); setOpaque(true); dim = new Dimension(64, 64); setMinimumSize(dim); setPreferredSize(new Dimension(128, 128)); setSize(dim); addComponentListener( new ComponentAdapter() { public void componentResized( ComponentEvent e ) { dim = getSize(); recalcScreenPoints(); repaint(); } }); final CurvePanel enc_this = this; mia = new MouseInputAdapter() { public void mouseClicked( MouseEvent e ) { DoublePoint pt; int index; CurvePoint cp; if( !isEnabled() ) return; // not while inactive if( e.getSource() == enc_this ) { //-------- Panel hit ------------------- if( (e.getClickCount() == 2) && !e.isAltDown() ) { pt = screenToParamSpace( e.getX(), e.getY() ); cp = new CurvePoint(); addPoint( cp, pt.x, pt.y ); } } else if( e.isAltDown() ) { index = getComponentIndex( (Component) e.getSource() ); if( (curve.getPoint( index - 1 ) != null) && (curve.getPoint( index + 1 ) != null) ) { // Endpunkte nicht loeschen removePoint( (CurvePoint) e.getSource() ); } } } public void mousePressed( MouseEvent e ) { Point cpLoc; ActionEvent parentE; if( !isEnabled() ) return; // not while running the operators // alten CurvePoint deselektieren currentCurvePoint.setSelected( CP_STATE_NORMAL ); if( e.getSource() == enc_this ) { //-------- Panel hit ------------------- if( currentCurvePoint != dummyPoint ) { currentCurvePoint = dummyPoint; // Listener benachrichtigen parentE = new ActionEvent( enc_this, ActionEvent.ACTION_PERFORMED, ACTION_POINTDESELECTED ); actionComponent.dispatchEvent( parentE ); } if( e.isControlDown() ) { // PopUp-Menu // XXX } } else { //-------- CurvePoint hit ------------------ currentCurvePoint = (CurvePoint) e.getSource(); cpLoc = currentCurvePoint.getLocation(); // neuen CurvePoint selektieren currentCurvePoint.setSelected( CP_STATE_SELECTED ); if( e.isControlDown() ) { // PopUp-Menu // XXX } else if( !e.isAltDown() ) { // prepare Drag dragLastX = e.getX() + cpLoc.x; dragLastY = e.getY() + cpLoc.y; dragPoint = screenToParamSpace( cpLoc.x + (CP_WIDTH>>1), cpLoc.y + (CP_HEIGHT>>1) ); dragType = DRAG_MOVE; } } } public void mouseReleased( MouseEvent e ) { Graphics g; ActionEvent parentE; if( !isEnabled() ) return; // not while running the operators if( dragState ) { // clear rubberband g = getGraphics(); g.setXORMode(getBackground()); g.setColor(Color.black); g.drawRect(dragLastX - (CP_WIDTH >> 1), dragLastY - (CP_HEIGHT >> 1), CP_WIDTH - 1, CP_HEIGHT - 1); movePoint(dragSource, dragPoint.x, dragPoint.y); dragState = false; setCursor(lastCursor); g.dispose(); } else { // Listener benachrichtigen if( (currentCurvePoint != dummyPoint) && !e.isAltDown() ) { parentE = new ActionEvent( enc_this, ActionEvent.ACTION_PERFORMED, ACTION_POINTSELECTED ); actionComponent.dispatchEvent( parentE ); } } dragType = DRAG_NONE; dragSource = null; } public void mouseDragged(MouseEvent e) { Graphics g; Point cpLoc; int currentX; int currentY; int index; DoublePoint currentPt; double dragPtX, dragPtY; if( !isEnabled() || (dragType == DRAG_NONE) ) return; dragSource = (CurvePoint) e.getSource(); cpLoc = dragSource.getLocation(); currentX = e.getX() + cpLoc.x; currentY = e.getY() + cpLoc.y; g = getGraphics(); g.setXORMode(getBackground()); g.setColor(Color.black); if( !dragState ) { // check if distance is ok to start drag if( (currentX-dragLastX)*(currentX-dragLastX) + (currentY-dragLastY)*(currentY-dragLastY) <= 16 ) return; // ...not ok synchronized( curve ) { index = getComponentIndex( dragSource ); dragLeft = curve.getPoint( index - 1 ); dragRight = curve.getPoint( index + 1 ); } lastCursor = getCursor(); setCursor( new Cursor( Cursor.CROSSHAIR_CURSOR )); dragState = true; } else { // clear rubberband g.drawRect( dragLastX - (CP_WIDTH>>1), dragLastY - (CP_HEIGHT>>1), CP_WIDTH-1, CP_HEIGHT-1 ); } // Werte korrigieren currentPt = screenToParamSpace( currentX, currentY ); dragPtX = currentPt.x; dragPtY = currentPt.y; if( (dragLeft != null) && (dragRight != null) ) { if( dragPtX <= dragLeft.x ) { dragPtX = dragLeft.x + curve.hSpace.inc; } if( dragPtX >= dragRight.x ) { dragPtX = dragRight.x - curve.hSpace.inc; } } else { dragPtX = dragPoint.x; // Endpunkte nicht horizontal verschieben } cpLoc = paramSpaceToScreen( dragPtX, dragPtY ); dragLastX = cpLoc.x; dragLastY = cpLoc.y; dragPoint = new DoublePoint( dragPtX, dragPtY ); // current one g.drawRect( dragLastX - (CP_WIDTH>>1), dragLastY - (CP_HEIGHT>>1), CP_WIDTH-1, CP_HEIGHT-1 ); g.dispose(); } }; addMouseListener(mia); addMouseMotionListener(mia); addPropertyChangeListener("enabled", new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent e) { ActionEvent parentE; if (currentCurvePoint != dummyPoint) { currentCurvePoint.setSelected(CP_STATE_NORMAL); currentCurvePoint = dummyPoint; // Listener benachrichtigen parentE = new ActionEvent(enc_this, ActionEvent.ACTION_PERFORMED, ACTION_POINTDESELECTED); actionComponent.dispatchEvent(parentE); } // setBackground( isEnabled() ? Color.white : null ); } }); setCurve(curve); setFocusable(false); } /** * Kurve zuweisen */ public void setCurve(Curve curve) { ActionEvent parentE; CurvePoint cp; clear(); synchronized (this.curve) { this.curve = (Curve) curve.clone(); currentCurvePoint = dummyPoint; for (int i = curve.size(); i > 0; i--) { cp = new CurvePoint(); cp.addMouseListener(mia); cp.addMouseMotionListener(mia); add(cp); } } recalcScreenPoints(); repaint(); // Listener benachrichtigen parentE = new ActionEvent(this, ActionEvent.ACTION_PERFORMED, ACTION_POINTDESELECTED); actionComponent.dispatchEvent(parentE); } /** * Kurve besorgen */ public Curve getCurve() { synchronized (curve) { return (Curve) curve.clone(); } } /** * Alle Punkte entfernen */ public void clear() { ActionEvent parentE; CurvePoint cp; synchronized (curve) { currentCurvePoint = dummyPoint; while (curve.size() > 0) { curve.removePoint(0); cp = (CurvePoint) getComponent(0); remove(cp); cp.removeMouseListener(mia); cp.removeMouseMotionListener(mia); } } repaint(); // Listener benachrichtigen parentE = new ActionEvent(this, ActionEvent.ACTION_PERFORMED, ACTION_POINTDESELECTED); actionComponent.dispatchEvent(parentE); } /** * Dimensionen aendern * erzeugt eine neue Kurve mit entsprechend skalierten Punkten * * @param hSpace neuer horizontaler Space, darf null sein * (= keine Aenderung) * @param vSpace neuer vertikaler Space, darf null sein * (= keine Aenderung) */ public void rescale(ParamSpace hSpace, ParamSpace vSpace) { ActionEvent parentE; Curve newCurve; DoublePoint pt ,pt2; double hScaling, vScaling; Vector<DoublePoint> revisit; int index; double dIndex; int ceil; synchronized( curve ) { if( hSpace == null) hSpace = curve.hSpace; if( vSpace == null) vSpace = curve.vSpace; // Scalierung berechnen hScaling = (hSpace.max - hSpace.min) / (curve.hSpace.max - curve.hSpace.min); vScaling = (vSpace.max - vSpace.min) / (curve.vSpace.max - curve.vSpace.min); // Punkte skalieren newCurve = new Curve( hSpace, vSpace, curve.type ); revisit = new Vector<DoublePoint>(); for( int i = curve.size() - 1; i >= 0; i-- ) { pt = curve.getPoint( i ); pt = new DoublePoint( (pt.x - curve.hSpace.min) * hScaling + hSpace.min, (pt.y - curve.vSpace.min) * vScaling + vSpace.min ); index = newCurve.addPoint( pt ); if( index < 0 ) { // passt nicht, erst mal merken revisit.addElement( pt ); } } for( int i = revisit.size() - 1; i >= 0; i-- ) { pt = revisit.elementAt( i ); dIndex = newCurve.indexOf( pt.x ); if( (dIndex >= 0.0) && (dIndex <= (double) (curve.size() - 1)) ) { ceil = (int) Math.ceil( dIndex ); pt2 = newCurve.getPoint( ceil ); if( pt2 != null ) { // nochmal mit leicht veraenderter Position versuchen index = newCurve.addPoint( pt2.x - newCurve.hSpace.inc, pt.y ); if( index < 0 ) { remove( 0 ); // ueberschuessigen CurvePoint entfernen } } } } this.curve = newCurve; } recalcScreenPoints(); repaint(); // Listener benachrichtigen parentE = new ActionEvent( this, ActionEvent.ACTION_PERFORMED, ACTION_SPACECHANGED ); actionComponent.dispatchEvent( parentE ); if( currentCurvePoint != dummyPoint ) { parentE = new ActionEvent( this, ActionEvent.ACTION_PERFORMED, ACTION_POINTCHANGED ); actionComponent.dispatchEvent( parentE ); } } /** * Derzeitigen horizontalen Space ermitteln */ public ParamSpace getHSpace() { synchronized (curve) { return curve.hSpace; } } /** * Derzeitigen vertikalen Space ermitteln */ public ParamSpace getVSpace() { synchronized (curve) { return curve.vSpace; } } /** * Registriert einen ActionListener; * Action-Events kommen, wenn sich der Wert des ParamFieldes aendert */ public void addActionListener( ActionListener list ) { actionComponent.addActionListener( list ); } /** * Entfernt einen ActionListener */ public void removeActionListener( ActionListener list ) { actionComponent.removeActionListener( list ); } /** * (Space)Koordinaten des aktuellen Punktes besorgen * * @return null, wenn Fehler oder kein Punkt angewaehlt */ public DoublePoint getPoint() { CurvePoint cp = currentCurvePoint; int index = getComponentIndex(cp); DoublePoint pt; if ((cp == dummyPoint) || (index < 0)) return null; synchronized (curve) { pt = curve.getPoint(index); return pt; } } /** * (Space)Koordinaten des aktuellen Punktes setzen * * @return false, wenn kein Punkt angewaehlt ist */ public boolean setPoint(DoublePoint pt) { CurvePoint cp = currentCurvePoint; if (cp == dummyPoint) return false; movePoint(cp, pt.x, pt.y); return true; } /** * Punkt auf das Panel legen * * @return null bei Fehler (z.B. schon Punkt am selben Ort vorhanden) */ public CurvePoint addPoint(CurvePoint cp, double x, double y) { ActionEvent parentE; int index; Point loc; cp.addMouseListener( mia ); cp.addMouseMotionListener( mia ); loc = paramSpaceToScreen( x, y ); cp.setLocation( loc.x - (CP_WIDTH>>1), loc.y - (CP_HEIGHT>>1) ); synchronized( curve ) { index = curve.addPoint( x, y ); if( index >= 0 ) { add( cp, index ); repaint(); // umschalten currentCurvePoint.setSelected( CP_STATE_NORMAL ); currentCurvePoint = cp; currentCurvePoint.setSelected( CP_STATE_SELECTED ); // Listener benachrichtigen parentE = new ActionEvent( this, ActionEvent.ACTION_PERFORMED, ACTION_POINTSELECTED ); actionComponent.dispatchEvent( parentE ); return cp; } else return null; } } /** * Punkt vom Panel entfernen */ public void removePoint(CurvePoint cp) { ActionEvent parentE; int index = getComponentIndex(cp); synchronized (curve) { if (index >= 0) { remove(cp); curve.removePoint(index); repaint(); } } currentCurvePoint.setSelected(CP_STATE_NORMAL); currentCurvePoint = dummyPoint; // Listener benachrichtigen parentE = new ActionEvent(this, ActionEvent.ACTION_PERFORMED, ACTION_POINTDESELECTED); actionComponent.dispatchEvent(parentE); cp.removeMouseListener(mia); cp.removeMouseMotionListener(mia); } /** * Punkt verschieben */ public void movePoint(CurvePoint cp, double x, double y) { ActionEvent parentE; Point loc; int index = getComponentIndex(cp); DoublePoint ptThis, ptLeft, ptRight; synchronized (curve) { ptThis = curve.getPoint(index); if (ptThis == null) return; ptLeft = curve.getPoint(index - 1); ptRight = curve.getPoint(index + 1); if ((ptLeft != null) && (ptRight != null)) { if (x <= ptLeft.x) { x = ptLeft.x + curve.hSpace.inc; } if( x >= ptRight.x ) { x = ptRight.x - curve.hSpace.inc; } } else { x = ptThis.x; // Endpunkte nicht horizontal verschieben } loc = paramSpaceToScreen( x, y ); cp.setLocation( loc.x - (CP_WIDTH>>1), loc.y - (CP_HEIGHT>>1) ); curve.removePoint( index ); curve.addPoint( x, y ); repaint(); } // Listener benachrichtigen parentE = new ActionEvent( this, ActionEvent.ACTION_PERFORMED, ACTION_POINTCHANGED ); actionComponent.dispatchEvent( parentE ); } public void paintComponent(Graphics g) { super.paintComponent(g); int hGrid, vGrid; int numPoints; Point pt1, pt2; Graphics2D g2 = (Graphics2D) g; g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // -------- Grid -------- hGrid = 1 << (int) Math.max( 0.0, Math.floor( Math.log( (float) dim.width / 20 ) / Constants.ln2 )); vGrid = 1 << (int) Math.max( 0.0, Math.floor( Math.log( (float) dim.height / 16 ) / Constants.ln2 )); g2.setPaint(isEnabled() ? (isDark ? pntGridDark : pntGridLight) : (isDark ? pntGridDarkD : pntGridLightD)); for (int i = 0; i <= hGrid; i++) { g2.drawLine((dim.width - 1) * i / hGrid, 0, (dim.width - 1) * i / hGrid, dim.height); } for (int i = 0; i <= vGrid; i++) { g2.drawLine(0, (dim.height - 1) * i / vGrid, dim.width, (dim.height - 1) * i / vGrid); } // -------- Punkt-Verbindungen -------- numPoints = getComponentCount(); if( numPoints >= 2 ) { g2.setPaint(isEnabled() ? (isDark ? pntLineDark : pntLineLight) : (isDark ? pntLineDarkD : pntLineLightD)); pt1 = getComponent( 0 ).getLocation(); if( curve.type == Curve.TYPE_DIA ) { // diagonal verbinden for( int i = 1; i < numPoints; i++, pt1 = pt2 ) { pt2 = getComponent( i ).getLocation(); g2.drawLine( pt1.x + (CP_WIDTH>>1), pt1.y + (CP_HEIGHT>>1), pt2.x + (CP_WIDTH>>1), pt2.y + (CP_HEIGHT>>1) ); } } else { // orthogonal verbinden for( int i = 1; i < numPoints; i++, pt1 = pt2 ) { pt2 = getComponent( i ).getLocation(); g2.drawLine( pt1.x + (CP_WIDTH>>1), pt1.y + (CP_HEIGHT>>1), pt2.x + (CP_WIDTH>>1), pt1.y + (CP_HEIGHT>>1) ); g2.drawLine( pt2.x + (CP_WIDTH>>1), pt1.y + (CP_HEIGHT>>1), pt2.x + (CP_WIDTH>>1), pt2.y + (CP_HEIGHT>>1) ); } } } } // -------- private methods -------- /* * Berechnet die Position der Punkt neu */ protected void recalcScreenPoints() { Component c; DoublePoint pt; Point loc; synchronized (curve) { for (int i = curve.size() - 1; i >= 0; i--) { c = getComponent(i); pt = curve.getPoint(i); loc = paramSpaceToScreen(pt.x, pt.y); c.setLocation(loc.x - (CP_WIDTH >> 1), loc.y - (CP_HEIGHT >> 1)); } } } /* * Uebersetzt Bildschirm-Koordinaten in Parameter-Koordinaten */ protected DoublePoint screenToParamSpace(int x, int y) { double dx, dy; synchronized (curve) { dx = curve.hSpace.min + (curve.hSpace.max - curve.hSpace.min) * x / (dim.width - 1); dy = curve.vSpace.max - (curve.vSpace.max - curve.vSpace.min) * y / (dim.height - 1); return new DoublePoint(curve.hSpace.fitValue(dx), curve.vSpace.fitValue(dy)); } } /* * Uebersetzt Parameter-Koordinaten in Bildschirm-Koordinaten */ protected Point paramSpaceToScreen(double dx, double dy) { int x, y; synchronized (curve) { x = (int) ((double) (dim.width - 1) * (dx - curve.hSpace.min) / (curve.hSpace.max - curve.hSpace.min)); y = (int) ((double) (dim.height - 1) * (curve.vSpace.max - dy) / (curve.vSpace.max - curve.vSpace.min)); return new Point(x, y); } } /* * Index auf dem Panel, -1 bei Fehler */ protected int getComponentIndex(Component c) { Component c2; for (int i = getComponentCount() - 1; i >= 0; i--) { c2 = getComponent(i); if (c2 == c) { return i; } } return -1; } // -------- interne CurvePoint-Klasse -------- private static class CurvePoint extends JPanel { // ........ private variables ........ // Status wie STATE_NORMAL, selektiert etc. private int state = CurvePanel.CP_STATE_UNKNOWN; // ........ public methods ........ protected CurvePoint() { super(null); Dimension dim = new Dimension(CurvePanel.CP_WIDTH, CurvePanel.CP_HEIGHT); setSelected(CurvePanel.CP_STATE_NORMAL); setSize(dim); setLocation(0, 0); // Event handling enableEvents(AWTEvent.MOUSE_EVENT_MASK); setFocusable(false); } /** * Status veraendern * * @return vorheriger Status */ protected int setSelected(int state) { int lastState = this.state; this.state = state; if (lastState != state) { if (state == CurvePanel.CP_STATE_NORMAL) { setForeground(SystemColor.control); setBackground(SystemColor.control); } else { setForeground(OpIcon.selectColor); setBackground(OpIcon.selectColor); } repaint(); } return lastState; } // private int isSelected() // { // return state; // } public void paintComponent(Graphics g) { super.paintComponent(g); g.draw3DRect(1, 1, CurvePanel.CP_WIDTH - 3, CurvePanel.CP_HEIGHT - 3, true); g.setColor(Color.black); g.drawRect(0, 0, CurvePanel.CP_WIDTH - 1, CurvePanel.CP_HEIGHT - 1); } // ........ private methods ........ protected void processMouseEvent(MouseEvent e) { if (e.getID() == MouseEvent.MOUSE_PRESSED) { requestFocus(); } super.processMouseEvent(e); } } // class CurvePoint }