f@0: /*
f@0: Cross-Modal DAW Prototype - Prototype of a simple Cross-Modal Digital Audio Workstation.
f@0:
f@0: Copyright (C) 2015 Queen Mary University of London (http://depic.eecs.qmul.ac.uk/)
f@0:
f@0: This program is free software: you can redistribute it and/or modify
f@0: it under the terms of the GNU General Public License as published by
f@0: the Free Software Foundation, either version 3 of the License, or
f@0: (at your option) any later version.
f@0:
f@0: This program is distributed in the hope that it will be useful,
f@0: but WITHOUT ANY WARRANTY; without even the implied warranty of
f@0: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
f@0: GNU General Public License for more details.
f@0:
f@0: You should have received a copy of the GNU General Public License
f@0: along with this program. If not, see .
f@0: */
f@0: package uk.ac.qmul.eecs.depic.daw.gui;
f@0:
f@0: import java.awt.BasicStroke;
f@0: import java.awt.Color;
f@0: import java.awt.Dimension;
f@0: import java.awt.Graphics;
f@0: import java.awt.Graphics2D;
f@0: import java.awt.RenderingHints;
f@0: import java.awt.Shape;
f@0: import java.awt.geom.Line2D;
f@0: import java.util.ArrayList;
f@0: import java.util.Collection;
f@0: import java.util.List;
f@0:
f@0: import javax.swing.event.ChangeEvent;
f@0: import javax.swing.event.ChangeListener;
f@0:
f@0: import uk.ac.qmul.eecs.depic.patterns.MathUtils;
f@0: import uk.ac.qmul.eecs.depic.patterns.Range;
f@0: import uk.ac.qmul.eecs.depic.patterns.Sequence;
f@0: import uk.ac.qmul.eecs.depic.patterns.SequenceEvent;
f@0: import uk.ac.qmul.eecs.depic.patterns.SequenceListener;
f@0:
f@2: /**
f@2: *
f@2: * A visual representation of a sequence graph. Here used for automation graphs overlaying the audio track.
f@2: *
f@2: * It listen to sequences (automations) and, when the sequence is updated, it updates itself
f@2: * and triggers the registered change listeners.
f@2: */
f@2: public class SequenceGraph implements SequenceListener {
f@0: private static final float SEQUENCE_LINE_WIDTH = 4.0f;
f@0: private static final int SEQUENCE_POINTS_CAPACITY = 30;
f@0:
f@0: private Sequence sequence;
f@0: private Color color;
f@0: private List sequencePoints;
f@0: private List sequenceLines;
f@0: private Dimension size;
f@0: private Collection listeners;
f@0: private float millisecPerPixel;
f@0:
f@0: public SequenceGraph(Color color, Dimension size) {
f@0: sequencePoints = new ArrayList<>(SEQUENCE_POINTS_CAPACITY);
f@0: sequenceLines = new ArrayList<>(SEQUENCE_POINTS_CAPACITY-1);
f@0: listeners = new ArrayList<>();
f@0: this.color = color;
f@0: this.size = size;
f@0: }
f@0:
f@0: public Dimension getSize(){
f@0: return size;
f@0: }
f@0:
f@0: public void setSize(Dimension size){
f@0: this.size = size;
f@0: }
f@0:
f@0: public void setColor(Color c){
f@0: this.color = c;
f@0: }
f@0:
f@0: public float getMillisecondsPerPixel(){
f@0: return millisecPerPixel;
f@0: }
f@0:
f@0: public void setMillisecPerPixel(float millisecPerPixel){
f@0: this.millisecPerPixel = millisecPerPixel;
f@0: }
f@0:
f@0: public Color getColor(){
f@0: return color;
f@0: }
f@0:
f@0: public List getSequencePoints(){
f@0: return sequencePoints;
f@0: }
f@0:
f@0: public List getSequenceLines(){
f@0: return sequenceLines;
f@0: }
f@0:
f@0: public void draw(Graphics g ){
f@0: if(sequenceLines.isEmpty()){
f@0: return;
f@0: }
f@0:
f@0: Graphics2D g2 = (Graphics2D)g;
f@0: g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
f@0: Color oldColor = g2.getColor();
f@0:
f@0: g2.setColor(color);
f@0:
f@0: /* draw sequence points and lines */
f@0: for(Shape point : sequencePoints){
f@0: g2.fill(point);
f@0: }
f@0:
f@0: for(Shape line : sequenceLines){
f@0: g2.fill(line);
f@0: }
f@0: /* restores previous state */
f@0: g2.setColor(oldColor);
f@0: g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
f@0: }
f@0:
f@0: public void addChangeListener(ChangeListener l){
f@0: listeners.add(l);
f@0: }
f@0:
f@0: public void removeChangeListener(ChangeListener l){
f@0: listeners.remove(l);
f@0: }
f@0:
f@0: protected void fireChangeListeners(){
f@0: ChangeEvent evt = new ChangeEvent(this);
f@0: for(ChangeListener l : listeners){
f@0: l.stateChanged(evt);
f@0: }
f@0: }
f@0:
f@0: @Override
f@0: public void sequenceUpdated(T evt) {
f@0: updateSequence();
f@0: }
f@0:
f@0: /**
f@0: *
f@0: * Sets a new sequence to be listened to. If the argument is {@code null}
f@0: * it stops listening to the previously set sequence, if any,m and clears all
f@0: * sequence points and lines. it calls {@code updateSequence}.
f@0: *
f@0: * @param s the new sequence.
f@0: */
f@0: public void setSequence(Sequence s){
f@0: /* remove this from the old sequence if any and add this to the new sequence */
f@0: if(sequence != null){
f@0: sequence.removeSequenceListener(this);
f@0: }
f@0:
f@0: if(s != null){
f@0: s.addSequenceListener(this);
f@0: }
f@0:
f@0: sequence = s;
f@0: updateSequence();
f@0: }
f@0:
f@0: public Sequence getSequence(){
f@0: return sequence;
f@0: }
f@0:
f@0: /**
f@0: * Recompute the sequence points and lines. It should be called whenever a change is done to this
f@0: * object. For example after {@code setSize} and {@code setMilliseconsPerPixel}.
f@0: *
f@0: */
f@0: public void updateSequence(){
f@0: /* clear previous values. will rebuild them if type is not NONE */
f@0: sequencePoints.clear();
f@0: sequenceLines.clear();
f@0:
f@0: if(sequence == null){
f@0: /* no sequence represented any more. Fire listeners with *
f@0: * sequencePoints and sequenceLines cleared */
f@0: fireChangeListeners();
f@0: return;
f@0: }
f@0:
f@0: /* height and width of this panel in order to draw proportionately */
f@0: int height = getSize().height;
f@0: int width = getSize().width;
f@0:
f@0: /* get the sequence range */
f@0: MathUtils.Scale scale = new MathUtils.Scale(sequence.getRange(),new Range(0.0f,(float)height));
f@0: BasicStroke stroke = new BasicStroke(SEQUENCE_LINE_WIDTH);
f@0:
f@0: /* if there are no sequence values, draw a straight line from the beginning to the end of the track */
f@0: if(sequence.getValuesNum() == 0){
f@0: sequenceLines.add(stroke.createStrokedShape(
f@0: new Line2D.Float(
f@0: 0f,
f@0: height - scale.linear(sequence.getBegin()),/* height is reversed as in swing the top is 0 */
f@0: width,
f@0: height - scale.linear(sequence.getEnd()))/* height is reversed as in swing the top is 0 */
f@0: ));
f@0: }else{
f@0: Sequence.Value previous = null;
f@0: /*previousX and previousY are the centre of sequence values calculated in the previous cycle */
f@0: int previousX = 0;
f@0: int previousY = 0;
f@0:
f@0: for(int i=0; i