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

Last change on this file since 1102 was 1102, checked in by davidb, 6 years ago

Reworking of the code-base to separate logic from graphics. This version of Expeditee now supports a JFX graphics as an alternative to SWING

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