source: trunk/src/org/apollo/gui/SampledTrackGraphView.java@ 1561

Last change on this file since 1561 was 1561, checked in by davidb, 3 years ago

A set of changes that spans three things: beat detection, time stretching; and a debug class motivated by the need to look at a canvas redraw issue most notable when a waveform widget is playing

File size: 26.2 KB
Line 
1package org.apollo.gui;
2
3import java.awt.BasicStroke;
4import java.awt.Color;
5import java.awt.Event;
6import java.awt.Font;
7import java.awt.GradientPaint;
8import java.awt.Graphics;
9import java.awt.Graphics2D;
10import java.awt.Paint;
11import java.awt.Point;
12import java.awt.Rectangle;
13import java.awt.event.ComponentEvent;
14import java.awt.event.ComponentListener;
15import java.util.LinkedList;
16import java.util.List;
17
18import javax.swing.JComponent;
19import javax.swing.SwingUtilities;
20
21import org.apollo.ApolloSystem;
22import org.apollo.audio.ApolloSubjectChangedEvent;
23import org.apollo.audio.SampledTrackModel;
24import org.apollo.audio.TrackSequence;
25import org.apollo.audio.util.PlaybackClock;
26import org.apollo.audio.util.SoundDesk;
27import org.apollo.audio.util.TrackMixSubject;
28import org.apollo.audio.util.PlaybackClock.PlaybackClockListener;
29import org.apollo.mvc.Observer;
30import org.apollo.mvc.Subject;
31import org.apollo.mvc.SubjectChangedEvent;
32import org.expeditee.core.Image;
33import org.expeditee.core.bounds.AxisAlignedBoxBounds;
34import org.expeditee.gio.swing.SwingConversions;
35import org.expeditee.gio.swing.SwingMiscManager;
36import org.expeditee.gui.Browser;
37import org.expeditee.gui.DisplayController;
38
39/**
40 * A graphical view for SampledTrackModel's.
41 *
42 * To use the view, just add a SampledTrackGraphView instance to a SampledTrackModel.
43 * Can re-use a SampledTrackGraphView instance.
44 *
45 * @author Brook Novak
46 */
47public class SampledTrackGraphView extends JComponent implements Observer, PlaybackClockListener {
48
49 private static final long serialVersionUID = 1L;
50
51 /** The observed subject. Can be null */
52 private SampledTrackModel trackModel = null;
53
54 /** The mix to use for the observed track. */
55 private TrackMixSubject trackMix = null;
56
57 /** The track sequence currently playing. Nullified when playback is stopped. */
58 private TrackSequence currentTrackSequence = null;
59
60 /** Negative = no playback */
61 private int currentPlaybackFramePosition = -1;
62
63 private WaveFormRenderProccessingUnit.WaveFormRenderTask renderTask = null;
64 private Image backBuffer = null; // Shared resource
65 private int bufferWidth = -1; // cached from backBuffer - so don't have to lock
66 private int bufferHeight = -1; // cached from backBuffer - so don't have to lock
67
68 // Model Data
69 private int timescaleFrameStart = 0; // in frames
70 private int timescaleFrameLength = 0; // in frames
71
72 private double timeStretchFactor = 1.0;
73
74 private final Font RENDER_MESSAGE_FONT = new Font("Arial", Font.BOLD | Font.ITALIC, 14);
75
76 private boolean alwaysFullViewOn = false;
77
78 public static final Color DEFAULT_BACKGROUND_COLOR = new Color(200, 200, 200);
79 public static final Color DEFAULT_BACKGROUND_HIGHTLIGHTS_COLOR = new Color(240, 240, 240);
80
81 private Color backColor = DEFAULT_BACKGROUND_COLOR;
82 private Color backColorHighlights = DEFAULT_BACKGROUND_HIGHTLIGHTS_COLOR;
83
84 public static final Color PLAYBACK_BAR_COLOR = new Color(225, 187, 15); // ******
85
86 /** The stroke used for drawing graph bars. E.G: The selection Start bar. */
87 public static final BasicStroke GRAPH_BAR_STROKE = SwingConversions.toSwingStroke(Strokes.SOLID_1);
88 public static final int GRAPH_BAR_NWIDTH = 1;
89
90 /** Used for safely invalidating playback positions at realtime. */
91 private Point expediteePosition;
92
93 private LinkedList<BackSection> backingSections = new LinkedList<BackSection>();
94 private EffecientInvalidator invalidator = null;
95 private List<TimelineChangeListener> timelineChangeListeners = new LinkedList<TimelineChangeListener>();
96
97 public SampledTrackGraphView() {
98
99 // Keep backings consistent with component resize
100 this.addComponentListener(new ComponentListener() {
101 public void componentHidden(ComponentEvent e) {
102 }
103
104 public void componentMoved(ComponentEvent e) {
105 //updateExpediteePoint(); // Doesn;t work here - swing has nasty bug
106 }
107
108 public void componentResized(ComponentEvent e) {
109
110 if (SampledTrackGraphView.this.trackModel != null) {
111
112 for (BackSection bs : backingSections) {
113 bs.updateBounds();
114 }
115 }
116 }
117
118 public void componentShown(ComponentEvent e) {
119 //updateExpediteePoint(); // Doesn;t work here - swing has nasty bug
120 }
121
122 });
123
124
125 }
126
127 /**
128 * {@inheritDoc}
129 */
130 @Override
131 public void setBounds(int x, int y, int width, int height) {
132 super.setBounds(x, y, width, height);
133 updateExpediteePoint(); // work around for Swing bug!
134 }
135
136
137 /**
138 * Saves this views position in expeditee space.
139 */
140 private void updateExpediteePoint() {
141 if (Browser._theBrowser == null) return;
142
143 expediteePosition = SwingUtilities.convertPoint(
144 this,
145 0, 0,
146 SwingMiscManager.getIfUsingSwingGraphicsManager().getContentPane());
147 }
148
149 /**
150 * Take care: can be null if not intialized.
151 * @return
152 * The point at which this view is in a expeditee frame.
153 */
154 protected Point getExpediteePoint() {
155 return expediteePosition;
156 }
157
158 public void addTimeScaleChangeListener(TimelineChangeListener tcl) {
159 if (!timelineChangeListeners.contains(tcl))
160 timelineChangeListeners.add(tcl);
161 }
162
163 protected void fireTimelineChanged() {
164 Event e = new Event(this, 0, null);
165 for (TimelineChangeListener listener : timelineChangeListeners)
166 listener.timelineChanged(e);
167 }
168
169 /**
170 * {@inheritDoc}
171 */
172 public Subject getObservedSubject() {
173 return trackModel;
174 }
175
176
177 /**
178 * @return
179 * The observed track model. Null if none is being viewed.
180 */
181 protected SampledTrackModel getSampledTrackModel() {
182 return trackModel;
183 }
184
185 /**
186 * {@inheritDoc}
187 */
188 public void setObservedSubject(Subject parent) {
189
190 // Note: parent can be an observer task. But that type of parent will never
191 // send null.
192 if (parent == null) { // Reset view to view nothing
193 trackModel = null;
194 if (trackMix != null) // avoid infinite recursion (deadlock until out of memory)
195 setMix(null); // stop observing the mix desk - otherwise this reference will never be garbage collected
196 releaseBuffer();
197
198 } else if (parent instanceof SampledTrackModel) { // Prepares the view
199
200 trackModel = (SampledTrackModel)parent;
201
202 if (alwaysFullViewOn) {
203 timescaleFrameStart = 0;
204 timescaleFrameLength = trackModel.getFrameCount();
205 fireTimelineChanged();
206 } else {
207 // Ensure that the timeline is valid - even though will probably change
208 clampCurrentTimescale();
209 }
210 }
211
212
213 }
214
215 /**
216 * {@inheritDoc}
217 */
218 public void modelChanged(Subject source, SubjectChangedEvent event) {
219 if (trackModel == null) return;
220
221
222 switch(event.getID()) {
223
224 // Invalidate dirty graph - according to render proccessor
225 case ApolloSubjectChangedEvent.RENDER_TASK_INVALIDATION_RECOMENDATION: // event from render proccessor
226 invalidateAll();
227 break;
228
229 case ApolloSubjectChangedEvent.AUDIO_REMOVED: // event from track model
230 case ApolloSubjectChangedEvent.AUDIO_INSERTED: // event from track model
231 if (alwaysFullViewOn) {
232 timescaleFrameStart = 0;
233 timescaleFrameLength = trackModel.getFrameCount();
234 fireTimelineChanged();
235 } else {
236 // Ensure that the timeline is valid - even though will probably change
237 clampCurrentTimescale();
238 }
239 redrawBuffer();
240 break;
241
242 case ApolloSubjectChangedEvent.TRACK_SEQUENCE_CREATED: // event from mix desk
243 assert(trackMix != null);
244 assert(event.getState() != null);
245
246 // OK a track sequence has just been created, but is it for this
247 // track mix?
248 if (event.getState().equals(trackMix.getChannelID())) {
249
250 // If so - then observe the created track sequence for playback messages
251 TrackSequence ts = SoundDesk.getInstance().getTrackSequence(trackMix.getChannelID());
252 assert(ts != null);
253 ts.addObserver(this); // NOTE: No need to remove self since these are garbage collected rather quickly
254 }
255
256 break;
257
258 case ApolloSubjectChangedEvent.PLAYBACK_STARTED: // event from track sequence
259
260 // Important to set here - since a track can be created but fail to initiate
261 // playback ... thus must set reference only when gauranteed that it can be unset.
262 currentTrackSequence = (TrackSequence)source;
263
264 PlaybackClock.getInstance().addPlaybackClockListener(this); // listen to ticks
265
266 onPlaybackStarted();
267 break;
268
269 case ApolloSubjectChangedEvent.PLAYBACK_STOPPED: // event from track sequence
270 PlaybackClock.getInstance().removePlaybackClockListener(this); // stop listening to ticks
271
272 int playbackFramePos = -1;
273
274 // The stop event might be due to a pause:
275 if (currentTrackSequence != null && trackMix != null &&
276 SoundDesk.getInstance().isPaused(trackMix.getChannelID())) {
277 playbackFramePos = currentTrackSequence.getSuspendedFrame();
278 }
279
280 // Has the user edited the audio such that the playback position is invalid?
281 if (playbackFramePos > trackModel.getFrameCount())
282 playbackFramePos = -1;
283
284 // Note: although these nullify the audio bytes when stopped, the state
285 // information carries a reference to the original track model at the moment
286 // which stores a lot of data and thus keeping the reference could
287 // hog the memory.
288 currentTrackSequence = null;
289
290 updatePlaybackPosition(playbackFramePos);
291
292 onPlaybackStopped();
293 break;
294
295
296 case ApolloSubjectChangedEvent.PAUSE_MARK_CHANGED:
297
298 if (trackMix != null && event.getState().equals(trackMix.getChannelID())) {
299
300 if (!SoundDesk.getInstance().isPaused(trackMix.getChannelID())) {
301 updatePlaybackPosition(-1);
302 }
303
304 }
305 break;
306
307 }
308 }
309
310 protected void onPlaybackStarted() {}
311 protected void onPlaybackStopped() {}
312
313 /**
314 * @return
315 * True if this view is viewing a track sequence that is playing
316 */
317 public final boolean isPlaying() {
318 return (currentTrackSequence != null && currentTrackSequence.isPlaying());
319 }
320
321 private int getPlaybackXPos(int playbackFrame) {
322
323 if (playbackFrame >= timescaleFrameStart &&
324 playbackFrame <= (timescaleFrameStart + timescaleFrameLength)) {
325
326 return XatFrame(playbackFrame);
327 }
328
329 return -1;
330 }
331
332 private void updatePlaybackPosition(int absFramePos) {
333
334 if (currentPlaybackFramePosition != absFramePos) {
335
336 int height = getHeight();
337 int viewWidth = getWidth();
338 int x;
339
340 // Invalidate old pos
341 if(expediteePosition != null) {
342
343 x = getPlaybackXPos(currentPlaybackFramePosition);
344
345 if (x >= 0 && x <= viewWidth) { // is the playback position in view?
346 DisplayController.invalidateArea(new AxisAlignedBoxBounds(
347 x + expediteePosition.x,
348 expediteePosition.y,
349 GRAPH_BAR_NWIDTH,
350 height));
351 }
352 }
353
354 // Set new pos
355 currentPlaybackFramePosition = absFramePos;
356
357 // Invalidate new pos
358 if(expediteePosition != null && currentPlaybackFramePosition >= 0) {
359
360 x = getPlaybackXPos(currentPlaybackFramePosition);
361
362 if (x >= 0 && x <= viewWidth) { // is the playback position in view?
363 DisplayController.invalidateArea(new AxisAlignedBoxBounds(
364 x + expediteePosition.x ,
365 expediteePosition.y,
366 GRAPH_BAR_NWIDTH,
367 height));
368
369 }
370 }
371
372 }
373
374
375 }
376
377 /**
378 * {@inheritDoc}
379 */
380 public void onTick(long framePosition, long msPosition) {
381
382 if (framePosition < 0) return; // Done
383
384 // The frame position / ms position is according to the software mixers timeline - not
385 // neccessarily starting off when the observed track sequence started. Thus must
386 // determine the actual frame position relative to the current rack sequence being played
387 // for this view:
388 int fpos = (int)(framePosition - currentTrackSequence.getInitiationFrame() + currentTrackSequence.getStartFrame());
389 if (fpos > currentTrackSequence.getEndFrame()) fpos = currentTrackSequence.getEndFrame();
390 updatePlaybackPosition(fpos);
391
392 // Note: the clock issues render requests
393 }
394
395 /**
396 * Sets the mix to use for playback / and to identify the channel for observering
397 * live playback...
398 *
399 * This resets to null whenever this view is set to no longer observe a track model.
400 *
401 * @param trackMix
402 * The new track mix to use. Null to use none.
403 *
404 */
405 public void setMix(TrackMixSubject trackMix) {
406
407 this.trackMix = trackMix;
408
409 if (trackMix != null) {
410 // Listen for track seq creation events ...
411 // thus if a track seq is created for this new ID then we can listen
412 // for playback events and render playback bars.
413 SoundDesk.getInstance().addObserver(this);
414
415 // If the mix is already playing - switch to a playing state.
416 if (SoundDesk.getInstance().isPlaying(trackMix.getChannelID())) {
417
418 currentTrackSequence = SoundDesk.getInstance().getTrackSequence(trackMix.getChannelID());
419
420 if (currentTrackSequence != null) {
421
422 // Note: that there are no race conditions because track sequence events
423 // are riased on this thread.
424 //currentPlaybackFramePosition = currentTrackSequence.getCurrentFrame(); // no invalidation
425 currentTrackSequence.addObserver(this);
426 PlaybackClock.getInstance().addPlaybackClockListener(this);
427 onPlaybackStarted();
428
429 }
430
431 } else if (SoundDesk.getInstance().isPaused(trackMix.getChannelID())){
432
433 int suspos = SoundDesk.getInstance().getLastPlayedFramePosition(trackMix.getChannelID());
434 updatePlaybackPosition(suspos);
435
436 }
437
438 } else {
439 SoundDesk.getInstance().removeObserver(this);
440 }
441
442 }
443
444 /**
445 * @return
446 * The mix set to be used for this view. Can be null.
447 */
448 public TrackMixSubject getMix() {
449 return trackMix;
450 }
451
452 public void setAlwaysFullView(boolean alwaysFullViewOn) {
453 this.alwaysFullViewOn = alwaysFullViewOn;
454 }
455
456 private void clampCurrentTimescale() {
457 int[] newTimeScale = getClampedTimeScale(timescaleFrameStart, timescaleFrameLength);
458
459 if (timescaleFrameStart != newTimeScale[0]
460 || timescaleFrameLength != newTimeScale[1]) {
461
462 timescaleFrameStart = newTimeScale[0];
463 timescaleFrameLength = newTimeScale[1];
464 fireTimelineChanged();
465 }
466 }
467
468 /**
469 * Calculates an appropriate timescale from the given start and length.
470 *
471 * trackModel Must not be null.
472 *
473 * @param frameStart
474 *
475 * @param frameLength
476 *
477 * @return
478 * Array of two ints. First element is clamped start frame,
479 * The second element is the clamped length.
480 */
481 private int[] getClampedTimeScale(int frameStart, int frameLength) {
482 assert(trackModel != null);
483
484 if (frameLength < 1) frameLength = 1;
485
486 if (frameLength > trackModel.getFrameCount()) {
487
488 frameLength = trackModel.getFrameCount();
489 frameStart = 0;
490
491 } else if (frameStart < 0) { // watch out for start
492
493 frameStart = 0;
494
495 } else if ((frameStart + frameLength) > trackModel.getFrameCount()) { // and end
496
497 frameStart = trackModel.getFrameCount() - frameLength;
498
499 }
500
501 assert (frameStart >= 0);
502 assert ((frameStart + frameLength) <= trackModel.getFrameCount());
503
504 return new int[] {frameStart, frameLength};
505 }
506
507 /**
508 * Sets the time scale for the graph viewport.
509 * As a result, the graph will be re-rendered asynchronously.
510 *
511 * @param frameStart
512 * @param frameLength
513 */
514 public void setTimeScale(int frameStart, int frameLength) {
515
516 int[] newScale = getClampedTimeScale(frameStart, frameLength);
517
518 if (newScale[0] == timescaleFrameStart && newScale[1] == timescaleFrameLength)
519 return;
520
521 // Set new model data
522 timescaleFrameStart = newScale[0];
523 timescaleFrameLength = newScale[1];
524
525 // Re-render graph with new time scale
526 redrawBuffer();
527
528 fireTimelineChanged();
529 }
530
531 /**
532 * @return
533 * The start <i>frame</i> of the current timescale.
534 * Always in valid position if subject is set.
535 *
536 */
537 public int getTimeScaleStart() {
538 return timescaleFrameStart;
539 }
540
541 /**
542 * @return
543 * The length of the timescale in <i>frames</i>.
544 * Always larger or equal to one if subject is set.
545 */
546 public int getTimeScaleLength() {
547 return timescaleFrameLength;
548 }
549
550
551 public void setTimeStretchFactor(double time_stretch_factor) {
552 timeStretchFactor = time_stretch_factor;
553 }
554
555 public double getTimeStretchFactor()
556 {
557 return timeStretchFactor;
558 }
559
560 /**
561 * Re-renders the waveform buffers to match the graph size if it needs to.
562 * This will avoid pixelation artifacts.
563 */
564 public void updateBuffer() {
565 // Check for resize of view or if not initialized yet
566 if (bufferWidth != getWidth() || bufferHeight != getHeight() || backBuffer == null) {
567 redrawBuffer();
568 }
569 }
570
571 /**
572 * Releases the image buffer.
573 *
574 * <b>Important:</b> Must call on the swing thread.
575 */
576 public void releaseBuffer() {
577
578 if (renderTask != null && !renderTask.isStopped())
579 WaveFormRenderProccessingUnit.getInstance().cancelTask(renderTask);
580
581 renderTask = null; // must be on swing thread because could be painting
582 if (backBuffer != null) backBuffer.releaseImage();
583 backBuffer = null; // must be on swing thread because could be painting
584 }
585
586 /**
587 * Initiates redrawing. If currently redrawing then it will be cancelled and will start from scratch.
588 * Re-creates the waveform buffer based on the panels current width and height.
589 *
590 * The graph panel will be fully invalidated
591 *
592 * If the trackModel is not set, then the buffer will not be redrawn.
593 *
594 */
595 protected void redrawBuffer() {
596
597 if (trackModel == null)
598 return;
599
600 int width = getWidth();
601 int height = getHeight();
602
603 if (width <= 0 || height <= 0) return;
604
605 // Remember width for re-rendering when graph size changes
606 bufferWidth = width;
607 bufferHeight = height;
608
609 // Cancel current rendering task
610 if (renderTask != null && !renderTask.isStopped())
611 WaveFormRenderProccessingUnit.getInstance().cancelTask(renderTask);
612
613 // Re-create the back buffer if need to
614 Object bufferLock = (backBuffer == null) ? new Object() : backBuffer;
615 synchronized(bufferLock) {
616
617 if (backBuffer == null ||
618 backBuffer.getWidth() != width
619 || backBuffer.getHeight() != height) {
620
621 // Create new sized buffer
622 backBuffer = Image.createImage(width, height);
623/* backBuffer = new BufferedImage(
624 width,
625 height,
626 BufferedImage.TYPE_BYTE_INDEXED,
627 ApolloColorIndexedModels.graphIndexColorModel);*/
628 }
629 }
630
631
632 // Describe render task
633 renderTask = WaveFormRenderProccessingUnit.getInstance().new WaveFormRenderTask(
634 backBuffer,
635 trackModel.getAllAudioBytes(),
636 trackModel.getFormat(),
637 trackModel.getBeats(),
638 timescaleFrameStart,
639 timescaleFrameLength,
640 true
641 );
642
643 // Wait for invalidation recommendation messages
644 renderTask.addObserver(this);
645
646 // Begin rendering ASAP
647 WaveFormRenderProccessingUnit.getInstance().queueTask(renderTask);
648 }
649
650 public void invalidateAll() {
651 invalidate(this.getBounds());
652 }
653
654 /**
655 * @param dirty
656 * The diry area with respect to this views origin... may want to
657 * do an efficient invalidation.
658 * For example if the parent of this is an iwidgets top most container
659 * then may want to explicitly invalidate rather than having to
660 * create an AWT message...
661 */
662 protected void invalidate(Rectangle dirty) {
663 if (invalidator != null) invalidator.onGraphDirty(this, dirty);
664 else super.repaint(); // the slower way
665 }
666
667 public void setInvalidator(EffecientInvalidator invalidator) {
668 this.invalidator = invalidator;
669 }
670
671 /**
672 * Adds a backing section to the graph - will be painted once visible.
673 * Does not invalidate the graph.
674 *
675 * Intended use: One BackSection for the lifetime of the instance.
676 * Just keep updateing the backing section.
677 *
678 * @param bs
679 */
680 protected void addBackingSection(BackSection bs) {
681
682 if (!backingSections.contains(bs)) {
683 backingSections.add(bs);
684 }
685 }
686
687 protected void removeBackingSection(BackSection bs) {
688 backingSections.remove(bs);
689 }
690
691 @Override
692 public void paint(Graphics g) {
693
694 // only performs rendering if widths are different.
695 // Also note that this is not invoked when widget is resizing because
696 // the widget is painted another way
697 updateBuffer(); // First paint will probably initialize the back buffer
698
699 // Draw backing sections
700 paintBacking((Graphics2D)g);
701
702 // Draw the wave forms
703 if (backBuffer != null) { // zoomed scale view
704
705 if (renderTask != null &&
706 (renderTask.isRendering()
707 || !renderTask.hasStarted())) {
708
709 // If rendering or pending - lock the buffer
710 synchronized(backBuffer) {
711
712 g.drawImage(SwingMiscManager.getIfUsingSwingImageManager().getInternalImage(backBuffer),
713 0,
714 0,
715 getWidth(),
716 getHeight(),
717 null);
718
719 }
720
721 // .. and give feedback to user about rendering progress
722 paintRenderProgress(g);
723
724 } else { // Not rendering
725
726 // No need to synchronize waveformBuffer because there is no thread accessing it,
727 // and a new render thread is always created on this thread.
728 g.drawImage(SwingMiscManager.getIfUsingSwingImageManager().getInternalImage(backBuffer),
729 0,
730 0,
731 getWidth(),
732 getHeight(),
733 null);
734
735 }
736
737 }
738
739 // Get the playback bar position
740 //int playbackFramePos = currentPlaybackFramePosition;
741
742 // If not playing but is suspended....
743 /*if (playbackFramePos < 0 && trackMix != null
744 && MixDesk.getInstance().isPaused(trackMix.getChannelID())) {
745
746 // Get the last playback pos - can be negitive if not played yet
747 playbackFramePos = MixDesk.getInstance().getLastPlayedFramePosition(trackMix.getChannelID());
748
749 // Has the user edited the audio such that the playback position is invalid?
750 if (playbackFramePos > trackModel.getFrameCount())
751 playbackFramePos = -1;
752 }*/
753
754 // Paint the playback bar
755 if (currentPlaybackFramePosition >= 0) {
756
757 int x = getPlaybackXPos(currentPlaybackFramePosition);
758 //System.out.println("*** playback x = " + x);
759
760 x = (int)Math.rint(((double)x) * getTimeStretchFactor()); // ******
761
762 if (x >= 0 && x <= getWidth()) {
763
764 ((Graphics2D)g).setStroke(GRAPH_BAR_STROKE);
765 g.setColor(PLAYBACK_BAR_COLOR);
766 g.drawLine(x, 0, x, getHeight());
767 }
768
769 }
770
771 if (getComponentCount() > 0) {
772 paintChildren(g);
773 }
774
775 }
776
777 private void paintBacking(Graphics2D g) {
778
779 // Clear background
780 Paint restore = g.getPaint();
781
782 if (ApolloSystem.useQualityGraphics) {
783
784 GradientPaint gp = new GradientPaint(
785 (getWidth() / 2), (int)(getHeight() * 0.8), backColor,
786 (getWidth() / 2), 0, backColorHighlights);
787 g.setPaint(gp);
788
789 } else {
790 g.setColor(backColor);
791 }
792
793 g.fillRect(0, 0, getWidth(), getHeight());
794
795 if (ApolloSystem.useQualityGraphics) {
796 g.setPaint(restore);
797 }
798
799 // Paint sections
800 for (BackSection bs : backingSections) {
801 bs.paint(g);
802 }
803
804 }
805
806 private void paintRenderProgress(Graphics g) {
807
808 g.setColor(Color.BLACK);
809
810 g.setFont(RENDER_MESSAGE_FONT);
811
812 String message = (renderTask.isRendering()) ? "Rendering..." : "Pending...";
813/*
814 Area clip = FrameGraphics.getCurrentClip();
815 Shape clipBackUp = g.getClip();
816 Area tmpClip = (clip != null) ? clip :
817 new Area(new Rectangle(0, 0,
818 Browser._theBrowser.getContentPane().getWidth(),
819 Browser._theBrowser.getContentPane().getHeight()));
820
821 Rectangle r = getBounds();
822 r.translate(expediteePosition.x, expediteePosition.y);
823 tmpClip.intersect(new Area(r));*/
824
825 // if (!tmpClip.isEmpty()) {
826
827 //g.setClip(tmpClip);
828 g.drawString(message, (getWidth() / 2) - 40,
829 (getHeight() / 2) + (RENDER_MESSAGE_FONT.getSize() / 2)); // center roughly
830 // g.setClip(clipBackUp);
831 //}
832
833 }
834
835
836
837 /**
838 * At a given x position in the graph, the matching frame number is returned.
839 * This considers the current timescale
840 *
841 * @param x
842 *
843 * @return Frame in track model
844 */
845 public int frameAtX(int x) {
846 return frameAtX(x, timescaleFrameStart, timescaleFrameLength);
847 }
848
849 private int frameAtX(int x, int frameStart, int frameLength) {
850
851 double viewPortPercent = x;
852 viewPortPercent /= getWidth();
853
854 // clamp
855 int frame = (int)(frameLength * viewPortPercent);
856 frame += frameStart;
857
858 if (frame >= (frameStart + frameLength))
859 frame = frameStart + frameLength;
860 else if(frame < frameStart)
861 frame = frameStart;
862
863 return frame;
864 }
865
866
867
868 /**
869 * At a given frame position in the track model, the matching x coordinate in the graph is returned.
870 * This considers the current timescale. Clamped to graph bounds.
871 *
872 * @param frame
873 *
874 * @return X pos in graph
875 */
876 public int XatFrame(long frame) {
877 return XatFrame(frame, timescaleFrameStart, timescaleFrameLength);
878 }
879
880 private int XatFrame(long frame, long frameStart, long frameLength) {
881
882 double framePercent = frame - frameStart;
883 framePercent /= frameLength;
884
885 int x = (int)(getWidth() * framePercent);
886
887 // clamp
888 if (x > getWidth()) x = getWidth();
889 else if (x < 0) x = 0;
890
891 return x;
892 }
893
894
895 /**
896 * A back section of a graph... intended for ranged highlighting.
897 *
898 * @author Brook Novak
899 */
900 protected class BackSection {
901
902 int left;
903
904 /** If less or equal to 1, then nothing is drawn. */
905 int width;
906
907 /** A flag for showing/hiding backing */
908 boolean visible;
909 Color color;
910
911 BackSection() {
912 left = 0;
913 width = 0;
914 visible = false;
915 color = Color.BLACK;
916 }
917
918 void paint(Graphics g) {
919 if (visible && width > 1) {
920 g.setColor(color);
921 g.fillRect(left, 0, width, getHeight());
922 }
923 }
924
925 /**
926 * Invoked when panel resized
927 */
928 void updateBounds() {
929 }
930
931 }
932
933 /**
934 * A SampledTrackGraphView can have a EffecientInvalidator where instead of
935 * invalidating via swing AWT event it invalidates via the implementor of
936 * a EffecientInvalidator if one is set.
937 *
938 * @author Brook Novak
939 *
940 */
941 public interface EffecientInvalidator
942 {
943 public void onGraphDirty(SampledTrackGraphView graph, Rectangle dirty);
944 }
945
946 public Color getBackColor()
947 {
948 return backColor;
949 }
950
951 /**
952 * Changes the background color and invalidates the graph.
953 * Doesnt invalidate if the color has not changed.
954 *
955 * @param backColor
956 * The new Background color. Must not be null.
957 *
958 */
959 public void setBackColor(Color backColor, Color highlights)
960 {
961 setBackColor(backColor, highlights, true);
962 }
963
964 /**
965 * Changes the background color.
966 *
967 * @param backColor
968 * The new Background color. Must not be null.
969 *
970 * @param invalidate
971 * True to invalidate the graph. False to miss invalidation.
972 * Doesnt invalidate if the color has not changed.
973 */
974 public void setBackColor(Color backColor, Color highlights, boolean invalidate)
975 {
976 if (backColor == null) throw new NullPointerException("backColor");
977 if (this.backColor != backColor || backColorHighlights != highlights) {
978 this.backColor = backColor;
979 backColorHighlights = highlights;
980 if (invalidate) invalidateAll();
981 }
982 }
983
984
985
986
987
988}
Note: See TracBrowser for help on using the repository browser.