/*
* TimelineScroll.java
* Eisenkraut
*
* Copyright (c) 2004-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:
* 12-May-05 re-created from de.sciss.meloncillo.timeline.TimelineScroll
* 15-Jul-05 fix in setPosition to avoid duplicate event generation
*/
package de.sciss.eisenkraut.timeline;
import de.sciss.app.AbstractApplication;
import de.sciss.app.DynamicAncestorAdapter;
import de.sciss.app.DynamicListening;
import de.sciss.app.DynamicPrefChangeManager;
import de.sciss.eisenkraut.gui.GraphicsUtil;
import de.sciss.eisenkraut.session.Session;
import de.sciss.eisenkraut.util.PrefsUtil;
import de.sciss.io.Span;
import javax.swing.*;
import java.awt.*;
import java.awt.event.AdjustmentEvent;
import java.awt.event.AdjustmentListener;
import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;
import java.util.prefs.PreferenceChangeEvent;
import java.util.prefs.PreferenceChangeListener;
/**
* A GUI element for allowing
* horizontal timeline scrolling.
* Subclasses <code>JScrollBar</code>
* simply to override the <code>paintComponent</code>
* method: an additional hairline is drawn
* to visualize the current timeline position.
* also a translucent hoverState rectangle is drawn
* to show the current timeline selection.
* <p>
* This class tracks the catch preferences
*
* TODO: the display properties work well
* with the Aqua look+and+feel, however
* are slightly wrong on Linux with platinum look+feel
* because the scroll gadgets have different positions.
*/
@SuppressWarnings("serial")
public class TimelineScroll
extends JScrollBar
implements AdjustmentListener, TimelineListener, DynamicListening, PreferenceChangeListener {
public static final int TYPE_UNKNOWN = 0;
public static final int TYPE_DRAG = 1;
public static final int TYPE_TRANSPORT = 2;
private final Session doc;
private Dimension recentSize = getMinimumSize();
private Shape shpSelection = null;
private Shape shpPosition = null;
private Span timelineSel = null;
private long timelineLen = 0;
private int timelineLenShift = 0;
private long timelinePos = 0;
private Span timelineVis = new Span();
private boolean prefCatch;
private final Object adjustmentSource = new Object();
private final Color colrSelection = GraphicsUtil.colrSelection();
private static final Color colrPosition = Color.red;
private static final Stroke strkPosition = new BasicStroke( 0.5f );
private final int trackMarginLeft;
private final int trackMargin;
private boolean wasAdjusting = false;
private boolean adjustCatchBypass = false;
private int catchBypassCount = 0;
private boolean catchBypassWasSynced= false;
/**
* Constructs a new <code>TimelineScroll</code> object.
* TODO: a clean way to determine the track rectangle ...
*
* @param doc session Session
*/
public TimelineScroll(Session doc) {
super(HORIZONTAL);
this.doc = doc;
LookAndFeel laf = UIManager.getLookAndFeel();
if( (laf != null) && laf.isNativeLookAndFeel() && (laf.getName().toLowerCase().contains("aqua")) ) {
trackMarginLeft = 6; // for Aqua look and feel
trackMargin = 39;
} else {
trackMarginLeft = 16; // works for Metal, Motif, Liquid, Metouia
trackMargin = 32;
}
timelineLen = doc.timeline.getLength();
timelineVis = doc.timeline.getVisibleSpan();
for (timelineLenShift = 0; (timelineLen >> timelineLenShift) > 0x3FFFFFFF; timelineLenShift++) ;
recalculateTransforms();
recalculateBoundedRange();
// --- Listener ---
new DynamicAncestorAdapter(this).addTo(this);
this.addAdjustmentListener(this);
new DynamicAncestorAdapter(new DynamicPrefChangeManager(AbstractApplication.getApplication().getUserPrefs(),
new String[]{PrefsUtil.KEY_CATCH}, this)).addTo(this);
setFocusable(false); // XXX TODO -- doesn't have effect with WebLaF ?
}
/**
* Paints the normal scroll bar using
* the super class's method. Additionally
* paints timeline position and selection cues
*/
public void paintComponent(Graphics g) {
super.paintComponent(g);
Dimension d = getSize();
Graphics2D g2 = (Graphics2D) g;
Stroke strkOrig = g2.getStroke();
Paint pntOrig = g2.getPaint();
if (d.width != recentSize.width || d.height != recentSize.height) {
recentSize = d;
recalculateTransforms();
}
if (shpSelection != null) {
g2.setColor(colrSelection);
g2.fill(shpSelection);
}
if (shpPosition != null) {
g2.setColor(colrPosition);
g2.setStroke(strkPosition);
g2.draw(shpPosition);
}
g2.setStroke(strkOrig);
g2.setPaint(pntOrig);
}
private void recalculateBoundedRange() {
final int len = (int) (timelineLen >> timelineLenShift);
final int len2 = (int) (timelineVis.getLength() >> timelineLenShift);
if (len > 0) {
if (!isEnabled()) setEnabled(true);
setValues((int) (timelineVis.getStart() >> timelineLenShift), len2, 0, len); // val, extent, min, max
setUnitIncrement(Math.max(1, (len2 >> 5))); // 1/32 extent
setBlockIncrement(Math.max(1, ((len2 * 3) >> 2))); // 3/4 extent
} else {
if (isEnabled()) setEnabled(false);
setValues(0, 100, 0, 100); // full view will hide the scrollbar knob
}
}
/*
* Calculates virtual->screen coordinates
* for timeline position and selection
*/
private void recalculateTransforms() {
double scale, x;
if (timelineLen > 0) {
scale = (double) (recentSize.width - trackMargin) / (double) timelineLen;
if (timelineSel != null) {
shpSelection = new Rectangle2D.Double(timelineSel.getStart() * scale + trackMarginLeft, 0,
timelineSel.getLength() * scale, recentSize.height);
} else {
shpSelection = null;
}
x = timelinePos * scale + trackMarginLeft;
shpPosition = new Line2D.Double(x, 0, x, recentSize.height);
} else {
shpSelection = null;
shpPosition = null;
}
}
/**
* Updates the red hairline representing
* the current timeline position in the
* overall timeline span.
* Called directly from TimelineFrame
* to improve performance. Don't use
* elsewhere.
*
* @param pos new position in absolute frames
* @param patience allowed graphic update interval
*
* @see java.awt.Component#repaint( long )
*/
public void setPosition(long pos, long patience, int type) {
if( prefCatch && (catchBypassCount == 0) /* && timelineVis.contains( timelinePos ) */ &&
((timelineVis.stop != timelineLen) || (pos < timelineVis.start)) &&
!timelineVis.contains( pos + (type == TYPE_TRANSPORT ? timelineVis.getLength() >> 3 : 0) )) {
timelinePos = pos;
long start;
final long stop;
start = timelinePos;
if (type == TYPE_TRANSPORT) {
start -= timelineVis.getLength() >> 3;
} else if (type == TYPE_DRAG) {
if (timelineVis.getStop() <= timelinePos) {
start -= timelineVis.getLength();
}
} else {
start -= timelineVis.getLength() >> 2;
}
stop = Math.min(timelineLen, Math.max(0, start) + timelineVis.getLength());
start = Math.max(0, stop - timelineVis.getLength());
if (stop > start) {
// it's crucial to update internal var timelineVis here because
// otherwise the delay between emitting the edit and receiving the
// change via timelineScrolled might be two big, causing setPosition
// to fire more than one edit!
timelineVis = new Span(start, stop);
doc.timeline.editScroll(this, timelineVis);
return;
}
}
timelinePos = pos;
recalculateTransforms();
repaint(patience);
}
public void addCatchBypass() {
if (++catchBypassCount == 1) {
catchBypassWasSynced = timelineVis.contains(timelinePos);
}
}
public void removeCatchBypass() {
if ((--catchBypassCount == 0) && catchBypassWasSynced) {
catchBypassWasSynced = false;
if (prefCatch && !timelineVis.contains(timelinePos)) {
long start;
final long stop;
start = timelinePos - (timelineVis.getLength() >> 2);
stop = Math.min(timelineLen, Math.max(0, start) + timelineVis.getLength());
start = Math.max(0, stop - timelineVis.getLength());
if (stop > start) {
// it's crucial to update internal var timelineVis here because
// otherwise the delay between emitting the edit and receiving the
// change via timelineScrolled might be two big, causing setPosition
// to fire more than one edit!
timelineVis = new Span(start, stop);
doc.timeline.editScroll(this, timelineVis);
}
}
}
}
// ---------------- DynamicListening interface ----------------
public void startListening() {
doc.timeline.addTimelineListener(this);
recalculateTransforms();
repaint();
}
public void stopListening() {
doc.timeline.removeTimelineListener(this);
}
// ---------------- PreferenceChangeListener interface ----------------
public void preferenceChange(PreferenceChangeEvent e) {
final String key = e.getKey();
final String value = e.getNewValue();
if (!key.equals(PrefsUtil.KEY_CATCH)) return;
prefCatch = Boolean.valueOf(value);
if (!prefCatch) return;
catchBypassCount = 0;
adjustCatchBypass = false;
if (!(timelineVis.contains(timelinePos))) {
long start = Math.max(0, timelinePos - (timelineVis.getLength() >> 2));
final long stop = Math.min(timelineLen, start + timelineVis.getLength());
start = Math.max(0, stop - timelineVis.getLength());
if (stop > start) {
doc.timeline.editScroll(this, new Span(start, stop));
}
}
}
// ---------------- TimelineListener interface ----------------
public void timelineSelected(TimelineEvent e) {
timelineSel = doc.timeline.getSelectionSpan();
recalculateTransforms();
repaint();
}
public void timelineChanged(TimelineEvent e) {
timelineLen = doc.timeline.getLength();
timelineVis = doc.timeline.getVisibleSpan();
for (timelineLenShift = 0; (timelineLen >> timelineLenShift) > 0x3FFFFFFF; timelineLenShift++) ;
recalculateTransforms();
recalculateBoundedRange();
repaint();
}
// ignored since the timeline frame will inform us
public void timelinePositioned(TimelineEvent e) { /* ignore */ }
public void timelineScrolled(TimelineEvent e) {
timelineVis = doc.timeline.getVisibleSpan();
if (e.getSource() != adjustmentSource) {
recalculateBoundedRange();
}
}
// ---------------- AdjustmentListener interface ----------------
// we're listening to ourselves
public void adjustmentValueChanged(AdjustmentEvent e) {
if (!isEnabled()) return;
final boolean isAdjusting = e.getValueIsAdjusting();
final Span oldVisi = doc.timeline.getVisibleSpan();
final Span newVisi = new Span( this.getValue() << timelineLenShift,
(this.getValue() + this.getVisibleAmount()) << timelineLenShift );
if( prefCatch && isAdjusting && !wasAdjusting ) {
adjustCatchBypass = true;
addCatchBypass();
} else if( wasAdjusting && !isAdjusting && adjustCatchBypass ) {
if( prefCatch && !newVisi.contains( timelinePos )) {
// we need to set prefCatch here even though laterInvocation will handle it,
// because removeCatchBypass might look at it!
prefCatch = false;
AbstractApplication.getApplication().getUserPrefs().putBoolean( PrefsUtil.KEY_CATCH, false );
}
adjustCatchBypass = false;
removeCatchBypass();
}
if( !newVisi.equals( oldVisi )) {
// if( prefCatch && oldVisi.contains( timelinePos ) && !newVisi.contains( timelinePos )) {
// AbstractApplication.getApplication().getUserPrefs().putBoolean( PrefsUtil.KEY_CATCH, false );
// }
doc.timeline.editScroll( adjustmentSource, newVisi );
}
wasAdjusting = isAdjusting;
}
}