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

Last change on this file since 1007 was 1007, checked in by davidb, 8 years ago

Generalization of audio support to allow playback/mixer to be stereo, plus some edits to comments

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