source: trunk/src_apollo/org/apollo/widgets/LinkedTrack.java@ 352

Last change on this file since 352 was 352, checked in by bjn8, 16 years ago

Improved widget linking... track-link coversion command

File size: 53.7 KB
Line 
1package org.apollo.widgets;
2
3import java.awt.BasicStroke;
4import java.awt.Color;
5import java.awt.Font;
6import java.awt.FontMetrics;
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.Shape;
14import java.awt.Stroke;
15import java.awt.event.ActionEvent;
16import java.awt.event.ActionListener;
17import java.awt.event.KeyEvent;
18import java.awt.event.KeyListener;
19import java.awt.event.MouseEvent;
20import java.awt.event.MouseListener;
21import java.awt.event.MouseMotionListener;
22import java.awt.geom.Area;
23import java.awt.geom.Rectangle2D;
24import java.util.LinkedList;
25import java.util.List;
26
27import javax.swing.JPanel;
28import javax.swing.JSlider;
29import javax.swing.JToggleButton;
30import javax.swing.SwingUtilities;
31
32import org.apollo.ApolloSystem;
33import org.apollo.AudioFrameMouseActions;
34import org.apollo.audio.ApolloSubjectChangedEvent;
35import org.apollo.audio.SampledAudioManager;
36import org.apollo.audio.structure.AbsoluteTrackNode;
37import org.apollo.audio.structure.AudioStructureModel;
38import org.apollo.audio.structure.LinkedTracksGraphNode;
39import org.apollo.audio.structure.OverdubbedFrame;
40import org.apollo.audio.structure.TrackGraphLoopException;
41import org.apollo.audio.util.FrameLayoutDaemon;
42import org.apollo.audio.util.MultiTrackPlaybackController;
43import org.apollo.audio.util.PlaybackClock;
44import org.apollo.audio.util.SoundDesk;
45import org.apollo.audio.util.Timeline;
46import org.apollo.audio.util.TrackMixSubject;
47import org.apollo.audio.util.MultiTrackPlaybackController.MultitrackLoadListener;
48import org.apollo.audio.util.PlaybackClock.PlaybackClockListener;
49import org.apollo.gui.ExpandedTrackManager;
50import org.apollo.gui.PlaybackControlPopup;
51import org.apollo.gui.SampledTrackGraphView;
52import org.apollo.io.AudioPathManager;
53import org.apollo.io.IconRepository;
54import org.apollo.items.EmulatedTextItem;
55import org.apollo.items.EmulatedTextItem.TextChangeListener;
56import org.apollo.mvc.Observer;
57import org.apollo.mvc.Subject;
58import org.apollo.mvc.SubjectChangedEvent;
59import org.apollo.util.AudioMath;
60import org.apollo.util.AudioSystemLog;
61import org.apollo.util.NullableLong;
62import org.apollo.util.ODFrameHeirarchyFetcher;
63import org.apollo.util.PopupReaper;
64import org.apollo.util.StringEx;
65import org.apollo.util.ODFrameHeirarchyFetcher.ODFrameReceiver;
66import org.expeditee.gui.Browser;
67import org.expeditee.gui.DisplayIO;
68import org.expeditee.gui.Frame;
69import org.expeditee.gui.FrameGraphics;
70import org.expeditee.gui.FrameUtils;
71import org.expeditee.gui.MessageBay;
72import org.expeditee.gui.MouseEventRouter;
73import org.expeditee.gui.PopupManager;
74import org.expeditee.items.Item;
75import org.expeditee.items.ItemParentStateChangedEvent;
76import org.expeditee.items.Text;
77import org.expeditee.items.widgets.InteractiveWidget;
78import org.expeditee.items.widgets.InteractiveWidgetInitialisationFailedException;
79import org.expeditee.items.widgets.InteractiveWidgetNotAvailableException;
80
81public class LinkedTrack extends InteractiveWidget implements ODFrameReceiver, Observer, MultitrackLoadListener, PlaybackClockListener {
82
83 private String virtualFilename; // immutable
84
85 private TrackMixSubject masterMix; // immutable
86
87 private OverdubbedFrame linkedOverdubbedFrame = null;
88
89 // NOTE: Using meta data as name model data - since there are two views for the name cannot store in a single label
90 private EmulatedTextItem nameLabel = null;
91
92 private PlaybackPopup playbackControlPopup = null;
93
94
95 // private SoundDeskPopup soundDeskPopup = null;
96 private MouseActions mouseActions = new MouseActions();
97 private MasterControlActionListener masterActionListener = new MasterControlActionListener();
98
99 /** All the track information for playing / editing / rendering.
100 * They ordered such that they are grouped by localfilename */
101 private List<Track> tracks = new LinkedList<Track>();
102 private int uniqueLocalTrackCount = 0; // the amount of different local filenames referenced by this track
103 private long totalFrameLength = 0;
104 private int currentPlaybackFramePosition = -1;
105
106
107 private int selectionStartX = 0;
108 private int selectionStart = 0;
109 private int selectionLength = -1;
110
111
112 private int state = NOT_INITIALIZED;
113 private int pendingGraphFetchCount = 0; // used for discarding redundant graph fetches
114 private String failMessage = null; // shown if in a failed state
115 private String abortMessage = null;
116
117 private static final Color FAILED_MESSAGE_COLOR = Color.RED;
118 private static final Font MESSAGE_FONT = TrackWidgetCommons.FREESPACE_TRACKNAME_FONT;
119 private static final Font HELPER_FONT = new Font("Arial", Font.BOLD, 12);
120
121 private static final Color[] TRACK_COLOR_WHEEL = {
122 new Color(66, 145, 255),
123 new Color(255, 163, 33),
124 new Color(155, 227, 48),
125 };
126 private static final Color TRACK_LOAD_COLOR = Color.BLACK;
127 private static final Color TRACK_FAIL_COLOR = Color.RED;
128
129 private static final Color SELECTION_COLOR = new Color(0,0,0,100);
130
131 private static final Stroke TRACK_BORDER = new BasicStroke(1.0f);
132 private static final Stroke TRACK_SELECTED_BORDER = new BasicStroke(2.0f);
133 private static final Color TRACK_BORDER_COLOR = Color.BLACK;
134 private static final Color TRACK_SELECTED_BORDER_COLOR = new Color(253, 255, 201);
135
136
137 /** States - mutex in this widget but not really .. for example since can be loading tracks and the graph at the same time... */
138 private static final int NOT_INITIALIZED = 0; // Not showing...
139 private static final int LOADING_TRACK_GRAPH = 1; // Loading the track layout information
140 private static final int FAILED_STATE = 2; // e.g. Bad graph... cannot use
141 private static final int STOPPED = 3; // Graph loaded and ready to play.
142 private static final int PLAYBACK_LOADING = 4; // Loading tracks from file/cache to play/resume
143 private static final int PLAYING = 5; // Playing audio.
144 private static final int EMPTY_STATE = 6; // e.g. No link set OR points to noththing to play
145
146 /** For parsing metadata */
147 public static final String META_VIRTUALNAME_TAG = "vn=";
148
149 /**
150 * Constructor called by Expeditee.
151 *
152 * @param source
153 * @param args
154 */
155 public LinkedTrack(Text source, String[] args) {
156 super(source, new JPanel(),
157 100, -1,
158 FrameLayoutDaemon.TRACK_WIDGET_HEIGHT, FrameLayoutDaemon.TRACK_WIDGET_HEIGHT);
159
160 // Read the metadata
161 virtualFilename = getStrippedDataString(META_VIRTUALNAME_TAG);
162
163 if (virtualFilename == null) { // virtualFilename is immutable. Must set on construction.
164 virtualFilename = AudioPathManager.generateVirtualFilename();
165 updateData(META_VIRTUALNAME_TAG, META_VIRTUALNAME_TAG + virtualFilename);
166 }
167
168 masterMix = SoundDesk.getInstance().getOrCreateMix(SoundDesk.createMasterChannelID(this));
169
170 // Set widget as a fixed size widget- using width from last recorded width in the meta data.
171 int width = getStrippedDataInt(TrackWidgetCommons.META_LAST_WIDTH_TAG, -1);
172 if (width >= 0 && width < FrameLayoutDaemon.MIN_TRACK_WIDGET_WIDTH) {
173 width = FrameLayoutDaemon.MIN_TRACK_WIDGET_WIDTH;
174 }
175
176 if (width < 0) {
177 setSize(-1, -1,
178 FrameLayoutDaemon.TRACK_WIDGET_HEIGHT, FrameLayoutDaemon.TRACK_WIDGET_HEIGHT,
179 FrameLayoutDaemon.TRACK_WIDGET_DEFAULT_WIDTH, FrameLayoutDaemon.TRACK_WIDGET_HEIGHT);
180 } else {
181 setSize(-1, -1,
182 FrameLayoutDaemon.TRACK_WIDGET_HEIGHT, FrameLayoutDaemon.TRACK_WIDGET_HEIGHT,
183 width, FrameLayoutDaemon.TRACK_WIDGET_HEIGHT);
184 }
185
186 playbackControlPopup = new PlaybackPopup();
187
188 nameLabel = new EmulatedTextItem(_swingComponent, new Point(10, 20));
189 nameLabel.setBackgroundColor(Color.WHITE);
190
191 String metaName = getStrippedDataString(TrackWidgetCommons.META_NAME_TAG);
192 if (metaName == null) metaName = "Untitled";
193 nameLabel.setText(metaName);
194
195 nameLabel.addTextChangeListener(new TextChangeListener() {
196
197 public void onTextChanged(Object source, String newLabel) {
198
199 if (state != PLAYING && state != STOPPED) return;
200
201 setName(nameLabel.getText());
202
203 }
204
205 });
206
207 _swingComponent.addKeyListener(new KeyListener() {
208
209 public void keyPressed(KeyEvent e) {
210 if ((state != PLAYING && state != STOPPED) || e.isControlDown()) return;
211 if (nameLabel.onKeyPressed(e, _swingComponent)) {
212 e.consume();
213 return;
214 }
215 }
216
217 public void keyReleased(KeyEvent e) {
218 if ((state != PLAYING && state != STOPPED) || e.isControlDown()) return;
219 if (nameLabel.onKeyReleased(e, _swingComponent)) {
220 e.consume();
221 return;
222 }
223 }
224
225 public void keyTyped(KeyEvent e) {
226 }
227
228 });
229
230
231 updateBorderColor();
232
233 setWidgetEdgeThickness(TrackWidgetCommons.STOPPED_TRACK_EDGE_THICKNESS);
234
235
236 }
237
238 /**
239 * Must set naem via this centralized method.
240 * @param newName
241 */
242 private void setName(String newName) {
243
244 String modelLabel = LinkedTrack.this.getStrippedDataString(TrackWidgetCommons.META_NAME_TAG);
245
246 if (!StringEx.equals(modelLabel, newName)) {
247 updateData(TrackWidgetCommons.META_NAME_TAG, newName);
248 }
249
250 if (!StringEx.equals(nameLabel.getText(), newName)) {
251 nameLabel.setText(newName);
252 }
253
254// if (soundDeskPopup != null &&
255// !StringEx.equals(soundDeskPopup.nameLabel.getText(), newName)) {
256// soundDeskPopup.nameLabel.setText(newName);
257// }
258
259 // Keep graph model consistant
260
261 Frame parent = getParentFrame();
262
263 AudioStructureModel.getInstance().onLinkedTrackWidgetNameChanged(
264 virtualFilename,
265 (parent != null) ? parent.getName() : null,
266 newName);
267
268 }
269
270 public boolean shouldLayout() {
271 return (state != EMPTY_STATE && state != FAILED_STATE);
272 }
273
274 /**
275 * Sets the GUI and all the popups to reflect the current state.
276 * Must be on the swing thread.
277 *
278 * @param newState
279 * The new state. The same state then it is ignored, unless
280 * its LOADING_TRACK_GRAPH - in that case the graph is reloaded.
281 */
282 private void setState(int newState) {
283 if (this.state == newState && newState != LOADING_TRACK_GRAPH) return; // no need to process request.
284
285 //assert ((state == NOT_INITIALIZED && !isVisible()) ||
286 // (state != NOT_INITIALIZED && isVisible()));
287
288 evaluate_state:
289
290 switch(newState) {
291 case NOT_INITIALIZED: // became invisible
292
293 playbackControlPopup.stopButton.setEnabled(false);
294 playbackControlPopup.rewindButton.setEnabled(false);
295 playbackControlPopup.playPauseButton.setEnabled(false);
296 tracks.clear();
297 break;
298
299 case LOADING_TRACK_GRAPH:
300
301 // Note: Can be invoked even if already in this state
302
303 // Get the frame that this widget is linking to
304 String linkedFrame = getAbsoluteLink(); // must be in famename form
305
306 if (linkedFrame != null) {
307
308 // Request a fetch from the model - eventually will return a result
309 // and the graph image will be build
310 pendingGraphFetchCount++;
311 ODFrameHeirarchyFetcher.getInstance().doFetch(linkedFrame, this);
312 // Once finished the callback will construct the model data / graph.
313 } else {
314 // No link - switch state to empty
315 newState = EMPTY_STATE;
316 break evaluate_state; // re-eval
317 }
318
319 playbackControlPopup.stopButton.setEnabled(false);
320 playbackControlPopup.rewindButton.setEnabled(false);
321 playbackControlPopup.playPauseButton.setEnabled(false);
322
323 break;
324
325 case FAILED_STATE:
326 assert(failMessage != null);
327 playbackControlPopup.stopButton.setEnabled(false);
328 playbackControlPopup.rewindButton.setEnabled(false);
329 playbackControlPopup.playPauseButton.setEnabled(false);
330
331 setSize(100, -1,
332 FrameLayoutDaemon.TRACK_WIDGET_HEIGHT, FrameLayoutDaemon.TRACK_WIDGET_HEIGHT,
333 200, FrameLayoutDaemon.TRACK_WIDGET_HEIGHT);
334
335 break;
336
337 case STOPPED:
338 playbackControlPopup.rewindButton.setEnabled(true);
339 playbackControlPopup.stopButton.setEnabled(false);
340 playbackControlPopup.playPauseButton.setEnabled(true);
341 playbackControlPopup.playPauseButton.setIcon(IconRepository.getIcon("play.png"));
342
343
344 setWidgetEdgeThickness(TrackWidgetCommons.STOPPED_TRACK_EDGE_THICKNESS);
345
346 break;
347
348 case PLAYBACK_LOADING:
349 playbackControlPopup.stopButton.setEnabled(false);
350 playbackControlPopup.rewindButton.setEnabled(false);
351 playbackControlPopup.playPauseButton.setEnabled(false);
352 break;
353
354 case PLAYING:
355 playbackControlPopup.stopButton.setEnabled(true);
356 playbackControlPopup.rewindButton.setEnabled(false);
357 playbackControlPopup.playPauseButton.setEnabled(true);
358 playbackControlPopup.playPauseButton.setIcon(IconRepository.getIcon("pause.png"));
359
360 setWidgetEdgeThickness(TrackWidgetCommons.PLAYING_TRACK_EDGE_THICKNESS);
361
362 PlaybackClock.getInstance().addPlaybackClockListener(this); // listen to ticks
363
364 break;
365
366 case EMPTY_STATE:
367 setSize(100, -1,
368 FrameLayoutDaemon.TRACK_WIDGET_HEIGHT, FrameLayoutDaemon.TRACK_WIDGET_HEIGHT,
369 200, FrameLayoutDaemon.TRACK_WIDGET_HEIGHT);
370 playbackControlPopup.stopButton.setEnabled(false);
371 playbackControlPopup.rewindButton.setEnabled(false);
372 playbackControlPopup.playPauseButton.setEnabled(false);
373 break;
374
375 default:
376 assert(false);
377
378 }
379
380 if (newState != PLAYING) { // safety
381 PlaybackClock.getInstance().removePlaybackClockListener(this);
382 }
383
384 // If in empty state then clear mouse listeners to give expeditee events
385 if (newState == EMPTY_STATE) {
386 _swingComponent.removeMouseListener(mouseActions);
387 _swingComponent.removeMouseMotionListener(mouseActions);
388
389 } else {
390
391 if (_swingComponent.getMouseListeners().length == 0) {
392 _swingComponent.addMouseListener(mouseActions);
393 }
394
395 if (_swingComponent.getMouseMotionListeners().length == 0) {
396 _swingComponent.addMouseMotionListener(mouseActions);
397 }
398 }
399
400 state = newState;
401
402 invalidateSelf();
403 FrameGraphics.refresh(true);
404 }
405
406 /**
407 * {@inheritDoc}
408 */
409 public Subject getObservedSubject() {
410 return null; // many
411 }
412
413 /**
414 * {@inheritDoc}
415 */
416 public void setObservedSubject(Subject parent) {
417 // many
418 }
419
420 /**
421 * {@inheritDoc}
422 */
423 public void modelChanged(Subject source, SubjectChangedEvent event) {
424
425 // Note: no need to check for audio structure model changing since can only change
426 // if in another frame.
427
428 // Synch GUI with track state
429 switch (event.getID()) {
430
431 case ApolloSubjectChangedEvent.PLAYBACK_STARTED:
432
433 if (state == PLAYBACK_LOADING && linkedOverdubbedFrame != null &&
434 MultiTrackPlaybackController.getInstance().isCurrentPlaybackSubject(
435 this.linkedOverdubbedFrame.getFrameName(), masterMix.getChannelID())) {
436 setState(PLAYING);
437 }
438 break;
439
440 case ApolloSubjectChangedEvent.PLAYBACK_STOPPED:
441
442 if (state == PLAYING) {
443 assert(linkedOverdubbedFrame != null);
444
445 int playbackFramePos = -1;
446
447 // The stop event might be due to a pause:
448 if (MultiTrackPlaybackController.getInstance().isMarkedAsPaused(
449 linkedOverdubbedFrame.getFrameName(), masterMix.getChannelID())) {
450 playbackFramePos = MultiTrackPlaybackController.getInstance().getLastSuspendedFrame();
451 }
452
453 // Has the user edited the audio such that the playback position is invalid?
454 if (playbackFramePos > totalFrameLength)
455 playbackFramePos = -1;
456
457 // Set the playback position to the exact suspended frame pos.
458 updatePlaybackPosition(playbackFramePos);
459
460 // Transition into new state
461 setState(STOPPED);
462
463 }
464 break;
465
466 case ApolloSubjectChangedEvent.VOLUME: // From obseved track mix
467 updateVolume();
468 break;
469
470 case ApolloSubjectChangedEvent.MUTE: // From obseved track mix
471 updateMute();
472 updateBorderColor();
473 break;
474
475 case ApolloSubjectChangedEvent.SOLO_PREFIX_CHANGED: // From mix desk
476 updateSolo();
477 updateBorderColor();
478 break;
479
480 }
481 }
482
483 /**
484 * {@inheritDoc}
485 */
486 public void onTick(long framePosition, long msPosition) {
487
488 if (framePosition < 0) return; // Done
489
490 // The frame position / ms position is according to the software mixers timeline - not
491 // neccessarily starting off when the observed multi track sequences started. Thus must
492 // determine the actual frame position relative to the current rack sequence being played
493 // for this view:
494 int fpos = (int)(framePosition
495 - MultiTrackPlaybackController.getInstance().getLastInitiationFrame()
496 + MultiTrackPlaybackController.getInstance().getLastStartFrame());
497
498 // Clamp
499 if (fpos > MultiTrackPlaybackController.getInstance().getLastEndFrame())
500 fpos = MultiTrackPlaybackController.getInstance().getLastEndFrame();
501
502 updatePlaybackPosition(fpos);
503
504 }
505
506 /**
507 * {@inheritDoc}
508 */
509 public void multiplaybackLoadStatusUpdate(int id, Object state) {
510
511 // NOTE: Could switch to an unloaded state
512 if (this.state != PLAYBACK_LOADING) return;
513
514 switch(id) {
515 case MultitrackLoadListener.LOAD_CANCELLED:
516 setState(STOPPED);
517 break;
518 case MultitrackLoadListener.LOAD_COMPLETE:
519 // Ensure that all tracks area dsiplayed and being loaded.
520 for (Track t : tracks) setTrackState(t, TRACKSTATE_READY);
521 invalidateSelf();
522 FrameGraphics.refresh(true);
523 break;
524 case MultitrackLoadListener.LOAD_FAILED_BAD_GRAPH:
525 abortMessage = "Graph contains loops";
526 ((Exception)state).printStackTrace();
527 break;
528 case MultitrackLoadListener.LOAD_FAILED_GENERIC:
529 abortMessage = "Unexpected error";
530 ((Exception)state).printStackTrace();
531 break;
532 case MultitrackLoadListener.LOAD_FAILED_PLAYBACK:
533 abortMessage = "Unable to aquire sound device";
534 break;
535 case MultitrackLoadListener.NOTHING_TO_PLAY:
536 abortMessage = "Nothing to play"; // could be due to user slecting empty space
537 break;
538 case MultitrackLoadListener.TRACK_LOAD_FAILED_IO:
539 // This is special... the loader does not abort... and it tries to load more.
540 setTrackState((String) state, TRACKSTATE_FAILED);
541 ((Exception)state).printStackTrace();
542 break;
543 case MultitrackLoadListener.TRACK_LOADED:
544 setTrackState((String) state, TRACKSTATE_READY);
545 invalidateSelf();
546 FrameGraphics.refresh(true);
547 break;
548
549 }
550
551 if (abortMessage != null) {
552 AudioSystemLog.println("Aborted playback - " + abortMessage);
553 setState(STOPPED);
554 }
555
556 }
557
558 /**
559 * {@inheritDoc}
560 */
561 @Override
562 protected String[] getArgs() {
563 return null;
564 }
565
566 /**
567 * {@inheritDoc}
568 */
569 @Override
570 protected List<String> getData() {
571
572 List<String> data = new LinkedList<String>();
573
574 data.add(META_VIRTUALNAME_TAG + virtualFilename);
575
576 String name = getName();
577
578 if (name != null)
579 data.add(TrackWidgetCommons.META_NAME_TAG + name);
580
581 data.add(TrackWidgetCommons.META_LAST_WIDTH_TAG + getWidth());
582
583 Long initTime = null;
584
585 Frame f = getParentFrame();
586 if (f != null) {
587 LinkedTracksGraphNode ltinf = AudioStructureModel.getInstance().getLinkedTrackGraphInfo(virtualFilename, f.getName());
588 if (ltinf != null) {
589 initTime = ltinf.getInitiationTime();
590 }
591 }
592
593 if (initTime == null) initTime = getInitiationTimeFromMeta(); // old meta
594 if (initTime == null) initTime = 0L;
595
596 data.add(TrackWidgetCommons.META_INITIATIONTIME_TAG + initTime);
597
598 return data;
599 }
600
601 /**
602 * {@inheritDoc}
603 */
604 @Override
605 public InteractiveWidget copy()
606 throws InteractiveWidgetNotAvailableException, InteractiveWidgetInitialisationFailedException {
607
608 Item source = getCurrentRepresentation().copy();
609 String clonedAnnotation = getAnnotationString();
610 source.setText(clonedAnnotation);
611
612 // But since this is a copy: must allocate a new virtual filename of its own...
613 // thus remove the vname meta
614 List<String> data = getData();
615
616 for (int i = 0; i < data.size(); i++) {
617 String str = data.get(i);
618 if (str.trim().startsWith(META_VIRTUALNAME_TAG)) {
619 data.remove(i);
620 i--; // continue - super safety to get rid of any hint of vname meta
621 }
622 }
623
624 source.setData(data);
625
626 return InteractiveWidget.createWidget((Text)source);
627
628 }
629
630 /**
631 * {@inheritDoc}
632 */
633 @Override
634 protected void onParentStateChanged(int eventType) {
635 super.onParentStateChanged(eventType);
636
637 Frame parent = this.getParentFrame();
638 String parentName = (parent != null) ? parent.getName() : null;
639
640 switch (eventType) {
641
642 case ItemParentStateChangedEvent.EVENT_TYPE_ADDED:
643
644 // Resume any layouts suspended by this track widget
645 FrameLayoutDaemon.getInstance().resumeLayout(this);
646
647 // Update model.
648 String link = getAbsoluteLink();
649
650
651 // Determine new initation time according to anchored position...
652 long initTime = getInitiationTimeFromMeta();
653
654 // If the user is restricting-y-axis movement then they might be moving
655 // this tracks Y-position only for layout reasons as opposed to repositioning
656 // where in the audio timeline the track should be. This must be accurate and
657 // avoid loosing the exact initiation time due to pixel-resolutoin issues
658 if (parent != null) {
659
660 boolean inferInitTime = true;
661
662 if (AudioFrameMouseActions.isYAxisRestictionOn()) {
663 Long ms = getInitiationTimeFromMeta();
664 if (ms != null) {
665 NullableLong timex = FrameLayoutDaemon.getInstance().getXAtMS(
666 ms,
667 parent);
668 if (timex != null && timex.getLongValue() == getX()) {
669 initTime = ms;
670 inferInitTime = false;
671 }
672 }
673 }
674
675 if (inferInitTime)
676 initTime = FrameLayoutDaemon.getInstance().getMSAtX(getX(), parent);
677 }
678
679 if (link != null) {
680
681 AudioStructureModel.getInstance().onLinkedTrackWidgetAnchored(
682 virtualFilename,
683 parentName,
684 initTime,
685 link,
686 getName(),
687 getY());
688
689
690 // Wakeup the daemon to note that it should recheck -- in the case that a track is
691 // added to a non-overdubbed frame the audio structure model will not bother
692 // adding the track until something requests for it.
693 FrameLayoutDaemon.getInstance().forceRecheck();
694 }
695
696 // Fall through ... and load the graph
697 case ItemParentStateChangedEvent.EVENT_TYPE_ADDED_VIA_OVERLAY: // TODO revise - and in sampled track widget
698 case ItemParentStateChangedEvent.EVENT_TYPE_SHOWN:
699 case ItemParentStateChangedEvent.EVENT_TYPE_SHOWN_VIA_OVERLAY:
700
701 // Setup observers
702 SoundDesk.getInstance().addObserver(this);
703 MultiTrackPlaybackController.getInstance().addObserver(this);
704 masterMix.addObserver(this);
705
706 // Load the graph
707 setState(LOADING_TRACK_GRAPH);
708
709 break;
710
711 case ItemParentStateChangedEvent.EVENT_TYPE_REMOVED:
712
713 // If yanking this from the frame into free space suspend the layout daemon
714 // for this frame
715 if (MouseEventRouter.getCurrentMouseEvent() != null &&
716 MouseEventRouter.getCurrentMouseEvent().getButton() == MouseEvent.BUTTON2) {
717 Frame suspended = DisplayIO.getCurrentFrame();
718 if (suspended != null) {
719 FrameLayoutDaemon.getInstance().suspendLayout(suspended, this);
720 }
721 }
722
723 // Update model.
724 AudioStructureModel.getInstance().onLinkedTrackWidgetRemoved(
725 virtualFilename,
726 parentName);
727
728 // Cancel loading of audio
729 if (state == PLAYBACK_LOADING) {
730 assert(linkedOverdubbedFrame != null);
731 MultiTrackPlaybackController.getInstance().cancelLoad(
732 linkedOverdubbedFrame.getFrameName(),
733 masterMix.getChannelID());
734 }
735
736
737 // TODO: Temp workaround - why are these widgets not refreshing?
738 SwingUtilities.invokeLater(new Runnable() {
739 public void run() {
740 invalidateSelf();
741 FrameGraphics.refresh(true);
742 }
743 }); // NOTE TO REPEAT: Pick up a linked track and delete it directly from freespace
744
745 case ItemParentStateChangedEvent.EVENT_TYPE_REMOVED_VIA_OVERLAY: // TODO revise - and in sampled track widget
746 case ItemParentStateChangedEvent.EVENT_TYPE_HIDDEN:
747
748 // Remove observers
749 SoundDesk.getInstance().removeObserver(this);
750 MultiTrackPlaybackController.getInstance().removeObserver(this);
751 masterMix.removeObserver(this);
752
753 setState(NOT_INITIALIZED);
754 break;
755
756 }
757
758 }
759
760
761
762 @Override
763 public void onDelete() {
764 super.onDelete();
765 // Resume any layouts suspended by this track widget
766 FrameLayoutDaemon.getInstance().resumeLayout(this);
767 }
768
769 /**
770 * {@inheritDoc}
771 */
772 @Override
773 public void setLink(String link, Text linker) {
774
775 String oldABSLink = getAbsoluteLink();
776
777 super.setLink(link, linker);
778
779 String newABSLink = getAbsoluteLink();
780
781 if (StringEx.equals(oldABSLink, newABSLink)) return;
782
783 Frame parent = this.getParentFrame();
784 String parentName = (parent != null) ? parent.getName() : null;
785
786 // Changing the link is the same as removing a track-link in the graph model (if had one)
787 AudioStructureModel.getInstance().onLinkedTrackWidgetRemoved(
788 virtualFilename,
789 parentName);
790
791 // And adding a new one (if applicable)
792 if (parent != null && newABSLink != null) {
793
794 Long initTime = (parent != null) ?
795 FrameLayoutDaemon.getInstance().getMSAtX(getX(), parent) :
796 getInitiationTimeFromMeta();
797
798 if (initTime == null) initTime = 0L;
799
800 String newName = (linker != null && linker.getText() != null) ? linker.getText() : getName();
801 if (newName != getName()) {
802 setName(newName);
803 }
804
805 AudioStructureModel.getInstance().onLinkedTrackWidgetAnchored(
806 virtualFilename,
807 parentName,
808 initTime, // TODO: Revise: actually would be better to get from model .. if possible
809 newABSLink,
810 newName,
811 getY());
812
813 }
814
815 // Reload track graph
816 setState(LOADING_TRACK_GRAPH);
817
818 }
819
820 /**
821 * {@inheritDoc}
822 */
823 @Override
824 protected void onSizeChanged() {
825 super.onSizeChanged();
826
827 // Re-adjust track sizes
828 updateGraphLayout();
829
830 }
831
832 /**
833 * @return
834 * The virtual filename. Never null.
835 */
836 public String getVirtualFilename() {
837 return virtualFilename;
838 }
839
840 /**
841 * Determines the running time from the meta data.
842 *
843 * @return
844 * The running time or this track in MS.
845 * -1 if unavilable.
846 */
847 public long getRunningMSTimeFromMeta() {
848 return getStrippedDataLong(TrackWidgetCommons.META_RUNNINGMSTIME_TAG, new Long(-1));
849 }
850
851 /**
852 * Determines the initiation time from the meta data.
853 *
854 * @return
855 * The initiation time or this track in MS.
856 * null if unavilable.
857 */
858 public Long getInitiationTimeFromMeta() {
859 return getStrippedDataLong(TrackWidgetCommons.META_INITIATIONTIME_TAG, null);
860 }
861
862
863 /**
864 * Adjusts the initiation time for this track - to the exact millisecond.
865 *
866 * The x-position is updated (which will be eventually done with possibly better
867 * accuracy if the layout daemon is on)
868 *
869 * @param specificInitTime
870 * The new initiation time for this track in milliseconds
871 */
872 public void setInitiationTime(long specificInitTime) {
873
874 Frame parent = getParentFrame();
875
876 // Update x position if it can
877 if (parent != null) {
878 Timeline tl = FrameLayoutDaemon.getInstance().getTimeline(parent);
879 if (tl != null) {
880 this.setPosition(tl.getXAtMSTime(specificInitTime), getY());
881 }
882 }
883
884 updateData(TrackWidgetCommons.META_INITIATIONTIME_TAG, TrackWidgetCommons.META_INITIATIONTIME_TAG + specificInitTime);
885
886 String absLink = getAbsoluteLink();
887
888 // If this track is in the audio model ...
889 if (AudioStructureModel.getInstance().getLinkedTrackGraphInfo(virtualFilename, (parent != null) ? parent.getName() : null) != null &&
890 absLink != null) {
891
892 FrameLayoutDaemon.getInstance().suspendLayout(DisplayIO.getCurrentFrame(), null);
893
894 try {
895
896 // Add and remove it
897 AudioStructureModel.getInstance().onLinkedTrackWidgetRemoved(virtualFilename, (parent != null) ? parent.getName() : null);
898
899 AudioStructureModel.getInstance().onLinkedTrackWidgetAnchored(
900 virtualFilename,
901 (parent != null) ? parent.getName() : null,
902 specificInitTime,
903 absLink,
904 getName(),
905 getY());
906
907 } finally {
908 FrameLayoutDaemon.getInstance().resumeLayout(null);
909 }
910
911 }
912 }
913
914 /**
915 * @return
916 * The name given to this widget... can be null.
917 */
918 public String getName() {
919
920 String name = nameLabel.getText();
921
922 if (name == null) {
923 name = getStrippedDataString(TrackWidgetCommons.META_NAME_TAG);
924 }
925
926 return name;
927 }
928
929 /**
930 * {@inheritDoc}
931 */
932 @Override
933 public void paint(Graphics g) {
934
935 if (Browser._theBrowser == null) return;
936
937 // Ensure that load bar or any text doesn't spill over widgets invalidation area
938 Area clip = FrameGraphics.getCurrentClip();
939
940 Shape clipBackUp = g.getClip();
941
942 Area tmpClip = (clip != null) ? clip :
943 new Area(new Rectangle(0, 0,
944 Browser._theBrowser.getContentPane().getWidth(),
945 Browser._theBrowser.getContentPane().getHeight()));
946
947 tmpClip.intersect(new Area(getBounds()));
948
949 if (tmpClip.isEmpty()) return;
950
951 g.setClip(tmpClip);
952
953 // Paint backing
954 g.setColor(SampledTrackGraphView.DEFAULT_BACKGROUND_COLOR);
955 g.fillRect(getX(), getY(), getWidth(), getHeight());
956
957 String centralizedMessage = null;
958
959 // Render according to the currently widget state
960 switch(state) {
961 case NOT_INITIALIZED: // became invisible
962 break;
963
964 case FAILED_STATE:
965 case EMPTY_STATE:
966 case LOADING_TRACK_GRAPH:
967
968 if (state == FAILED_STATE) {
969 centralizedMessage = failMessage;
970 assert(failMessage != null);
971 g.setColor(FAILED_MESSAGE_COLOR);
972 } else if (state == LOADING_TRACK_GRAPH){
973 centralizedMessage = "Loading...";
974 g.setColor(Color.DARK_GRAY);
975 } else { // empty
976 if (getLink() == null) {
977 centralizedMessage = "Click to create";
978 } else {
979 centralizedMessage = "Nothing to play";
980 }
981
982 g.setColor(Color.DARK_GRAY);
983 }
984
985 break;
986
987 case STOPPED:
988 case PLAYBACK_LOADING:
989 case PLAYING:
990 int x = getX();
991 int y = getY();
992 Track selected = null;
993
994 for (Track track : tracks) {
995 track.paintTrackArea((Graphics2D)g, x, y);
996 if (track.isSelected) {
997 selected = track;
998 } else {
999 track.paintTrackBorder(g, x, y);
1000
1001 if (track.area.height > 15) { // don't clutter up busy linked tracks
1002 drawTrackName(track, g, Color.DARK_GRAY);
1003 }
1004
1005 }
1006
1007 }
1008
1009 // Selection can spill over other track areas so to avoid z fighting always ensure that
1010 // the selected border is drawn last
1011 if (selected != null) {
1012 selected.paintTrackBorder(g, x, y);
1013 drawTrackName(selected, g, Color.BLACK);
1014 }
1015
1016 if (abortMessage != null) {
1017 String name = getName();
1018 if (name == null) name = "Unamed";
1019 MessageBay.errorMessage(name + " linked track: " + abortMessage);
1020 }
1021
1022 if (state == PLAYBACK_LOADING) {
1023
1024 centralizedMessage = "Loading dubs...";
1025 g.setColor(Color.BLACK);
1026
1027 } else {
1028
1029 // Paint the selection range
1030 int selLeftX = XatFrame(selectionStart);
1031 if (selectionLength > 1) {
1032 g.setColor(SELECTION_COLOR);
1033 g.fillRect( getX() + selLeftX,
1034 getY(),
1035 XatFrame(selectionStart + selectionLength) - selLeftX,
1036 getHeight());
1037 }
1038
1039 // Draw selection start bar
1040 ((Graphics2D)g).setStroke(SampledTrackGraphView.GRAPH_BAR_STROKE);
1041
1042 // Note that if the start line is near the edges of the panel it can be concealed - thus
1043 // set a thick line on the edges
1044 x = selLeftX + getX();
1045 if (x == 0) {
1046 x = 1;
1047 } else if (x == getWidth()){
1048 x = getWidth() - 1;
1049 }
1050
1051 g.setColor(Color.RED);
1052 g.drawLine(
1053 x,
1054 getY(),
1055 x,
1056 getY() + getHeight());
1057
1058
1059 // Paint the playback bar
1060 if (currentPlaybackFramePosition >= 0 && linkedOverdubbedFrame != null) {
1061
1062 x = XatFrame(currentPlaybackFramePosition) + getX();
1063
1064 ((Graphics2D)g).setStroke(SampledTrackGraphView.GRAPH_BAR_STROKE);
1065 g.setColor(SampledTrackGraphView.PLAYBACK_BAR_COLOR);
1066 g.drawLine(x, getY(), x, getY() + getHeight());
1067
1068 }
1069
1070 }
1071
1072
1073
1074 break;
1075
1076 }
1077
1078 // Draw centralized message string
1079 if (centralizedMessage != null) {
1080
1081 FontMetrics fm = g.getFontMetrics(MESSAGE_FONT);
1082 Rectangle2D rect = fm.getStringBounds(centralizedMessage, g);
1083
1084 // Calc centered position
1085 int x = getX() + ((getWidth() / 2) - (int)(rect.getWidth() / 2));
1086 int y = getY() + ((getHeight() / 2) + (int)(rect.getHeight() / 2) - 4);
1087
1088 // Draw text
1089 g.setFont(MESSAGE_FONT);
1090 g.drawString(centralizedMessage, x, y);
1091
1092 // Draw add icon if waiting for a link
1093 if (getLink() == null) {
1094 IconRepository.getIcon("goto.png").paintIcon(null,
1095 g,
1096 x - 34,
1097 getY() + (getHeight() / 2) - 12);
1098 }
1099
1100 }
1101
1102 // Paint name label over everything
1103 if (state != NOT_INITIALIZED && state != FAILED_STATE && state != EMPTY_STATE)
1104 nameLabel.paint(g);
1105
1106 g.setClip(clipBackUp);
1107
1108 super.paintLink((Graphics2D)g);
1109
1110 }
1111
1112 private void drawTrackName(Track track, Graphics g, Color textColor) {
1113
1114 // Draw helper message for user to give meaningful information of what they
1115 // are actually highlighting
1116 String helperMessage = track.getTrackName();
1117 if (helperMessage == null) helperMessage = "<Unamed>";
1118 helperMessage = helperMessage + " (" + track.getParentFrameName() + ")";
1119
1120 FontMetrics fm = g.getFontMetrics(HELPER_FONT);
1121 Rectangle2D rect = fm.getStringBounds(helperMessage, g);
1122
1123 // Calc centered position on track bounds -- which can be smaller than the font bounds`
1124 int xoff = (int)((track.area.width - rect.getWidth()) / 2);
1125 int yoff = (int)((track.area.height / 2) + (rect.getHeight() / 2));
1126
1127 xoff += (getX() + track.area.x);
1128 yoff += (getY() + track.area.y);
1129
1130 if (xoff < getX())
1131 xoff = getX();
1132 else if ((xoff + rect.getWidth()) > (getX() + getWidth()))
1133 xoff = (int)(getX() + getWidth() - rect.getWidth());
1134
1135 // Draw text
1136 g.setColor(textColor);
1137 g.setFont(HELPER_FONT);
1138 g.drawString(helperMessage, xoff, yoff);
1139
1140 }
1141
1142 @Override
1143 protected void paintInFreeSpace(Graphics g) {
1144
1145 super.paintInFreeSpace(g);
1146
1147 Shape clipBackUp = g.getClip();
1148 Rectangle tmpClip = (clipBackUp != null) ? clipBackUp.getBounds() :
1149 new Rectangle(0, 0,
1150 Browser._theBrowser.getContentPane().getWidth(),
1151 Browser._theBrowser.getContentPane().getHeight());
1152
1153 g.setClip(tmpClip.intersection(getBounds()));
1154
1155 // Draw the name
1156 String name = getName();
1157 if (name == null) {
1158 name = "Unnamed";
1159 }
1160
1161 g.setFont(TrackWidgetCommons.FREESPACE_TRACKNAME_FONT);
1162 g.setColor(TrackWidgetCommons.FREESPACE_TRACKNAME_TEXT_COLOR);
1163
1164 // Center track name
1165 FontMetrics fm = g.getFontMetrics(TrackWidgetCommons.FREESPACE_TRACKNAME_FONT);
1166 Rectangle2D rect = fm.getStringBounds(name, g);
1167
1168 g.drawString(
1169 name,
1170 this.getX() + (int)((getWidth() - rect.getWidth()) / 2),
1171 this.getY() + (int)((getHeight() - rect.getHeight()) / 2) + (int)rect.getHeight()
1172 );
1173
1174 g.setClip(clipBackUp);
1175
1176
1177 }
1178
1179 private void updatePlaybackPosition(int absFramePos) {
1180
1181 if (currentPlaybackFramePosition != absFramePos) {
1182
1183 int height = getHeight();
1184 int offsetX = getX();
1185 int offsetY = getY();
1186 int x;
1187
1188 // Invalidate old pos
1189 if (currentPlaybackFramePosition >= 0) {
1190 x = XatFrame(currentPlaybackFramePosition);
1191
1192 FrameGraphics.invalidateArea(new Rectangle(
1193 x + offsetX,
1194 offsetY,
1195 SampledTrackGraphView.GRAPH_BAR_NWIDTH, // be consistent
1196 height));
1197 }
1198
1199 // Set new pos
1200 currentPlaybackFramePosition = absFramePos;
1201
1202 if (currentPlaybackFramePosition >= 0) {
1203
1204 // Invalidate new pos
1205 x = XatFrame(currentPlaybackFramePosition);
1206
1207 FrameGraphics.invalidateArea(new Rectangle(
1208 x + offsetX ,
1209 offsetY,
1210 SampledTrackGraphView.GRAPH_BAR_NWIDTH, // be consistent
1211 height));
1212 }
1213
1214 }
1215
1216
1217 }
1218
1219 private int XatFrame(int frame) {
1220 float width = getWidth();
1221 float div = totalFrameLength;
1222 div = ((float)frame) / div;
1223 return (int)(width * div);
1224 }
1225
1226 private int frameAtX(int x) {
1227 float totalFrames = totalFrameLength;
1228 float div = getWidth();
1229 div = ((float)x) / div;
1230 return (int)(totalFrames * div);
1231 }
1232
1233 /**
1234 * {@inheritDoc}
1235 * Once a result is received, the graph is build
1236 */
1237 public void receiveResult(OverdubbedFrame odFrame, TrackGraphLoopException loopEx) {
1238
1239 linkedOverdubbedFrame = odFrame;
1240
1241 pendingGraphFetchCount --;
1242
1243 // If there are new requests pending for rebuilding the graph then leave the state
1244 // as loading graph.
1245 if (pendingGraphFetchCount > 0) {
1246 linkedOverdubbedFrame = null;
1247 return;
1248 }
1249
1250 // Reset tracks
1251 tracks.clear();
1252 uniqueLocalTrackCount = 0;
1253 totalFrameLength = 0;
1254
1255 // Only continue to build the graph iff this widget is still actaully visible
1256 // If allowed the graph to build, then could consume up precious memory :(
1257 if (!isVisible()) return;
1258
1259 // Before any construction of a graph - the requested graph is only for the overdubbed
1260 // frame that this widget is linking to. However, this linked widget may not be
1261 // at all valid, that is, it might be the cause of a loop! If it is then this widget
1262 // would be un-usable.
1263 Frame parent = getParentFrame();
1264 String parentFrameName = (parent != null) ? parent.getName() : null;
1265 if (parentFrameName != null) {
1266 OverdubbedFrame odframe = AudioStructureModel.getInstance().getOverdubbedFrame(parentFrameName);
1267 if (odframe != null && !odframe.containsLinkedTrack(virtualFilename)) {
1268 failMessage = "Bad link: contains a loop";
1269 setState(FAILED_STATE);
1270 return;
1271 }
1272 }
1273
1274 if (loopEx != null) {
1275 failMessage = "Bad link: contains a loop";
1276 setState(FAILED_STATE);
1277 } else if (odFrame == null) { // In this case either the frame does not exist or is an empty OD frame
1278 setState(EMPTY_STATE);
1279 } else {
1280
1281 // Infer total frame length
1282 totalFrameLength = AudioMath.millisecondsToFrames(
1283 odFrame.calculateRunningTime(), SampledAudioManager.getInstance().getDefaultPlaybackFormat());
1284
1285 List<AbsoluteTrackNode> absNodes = odFrame.getAbsoluteTrackLayoutDeep(
1286 masterMix.getChannelID());
1287
1288 if (absNodes.isEmpty()) {
1289 setState(EMPTY_STATE);
1290 } else {
1291
1292 while (!absNodes.isEmpty()) {
1293
1294 // Group tracks in list by type so can be rendered such that they look
1295 // like they represent the same audio... but also retaining the virtual y
1296 // ordering...
1297 String localFilename = absNodes.get(0).getTrackNode().getLocalFilename();
1298
1299 for (int i = 0; i < absNodes.size(); i++) {
1300 AbsoluteTrackNode node = absNodes.get(i);
1301 if (node.getTrackNode().getLocalFilename().equals(localFilename)) {
1302 tracks.add(new Track(absNodes.remove(i)));
1303 i--;
1304 }
1305 }
1306
1307 uniqueLocalTrackCount++;
1308
1309 }
1310
1311 // Update the layout
1312 updateGraphLayout();
1313
1314 // Evaluate the state of this add set the state accordingly
1315 if (MultiTrackPlaybackController.getInstance().isLoading(
1316 linkedOverdubbedFrame.getFrameName(),
1317 masterMix.getChannelID())) {
1318
1319 // Ensure that am receiving notifiactions:
1320 List<String> loaded = MultiTrackPlaybackController.getInstance().attachLoadListener(this);
1321 assert(loaded != null);
1322
1323 setState(PLAYBACK_LOADING);
1324
1325 // Update GUI according to current load state
1326 for (String ln : loaded)
1327 setTrackState(ln, TRACK_LOADED);
1328
1329 } else if (MultiTrackPlaybackController.getInstance().isPlaying(
1330 linkedOverdubbedFrame.getFrameName(),
1331 masterMix.getChannelID())) {
1332
1333 setState(PLAYING);
1334
1335 } else {
1336 setState(STOPPED);
1337 }
1338
1339
1340 }
1341
1342
1343
1344 }
1345
1346 }
1347
1348 /**
1349 * Lays out any tracks to fix the current widget size.
1350 */
1351 private void updateGraphLayout() {
1352
1353 if (tracks.isEmpty() || !this.isVisible() || linkedOverdubbedFrame == null)
1354 return;
1355
1356 long totalRunningTime = linkedOverdubbedFrame.calculateRunningTime();
1357
1358 // Tracks are assumed to be ordered such that they are grouped. (If not then
1359 // graph may look confusing).
1360 String groupLocalFilename = tracks.get(0).getLocalFilename();
1361 int currentColorIndex = 0;
1362 float currentY = 0;
1363
1364 // Helpers:
1365 int width = getWidth();
1366 int height = getHeight();
1367
1368 float trackHeight = (float)height / uniqueLocalTrackCount;
1369 int nTrackHeight = (int)trackHeight;
1370 if (nTrackHeight == 0) nTrackHeight = 1;
1371
1372 for (Track track : tracks) {
1373
1374 float x = track.getABSStartTime();
1375 x /= totalRunningTime;
1376
1377 float trackWidth = track.getRunningTime();
1378 trackWidth /= totalRunningTime;
1379
1380 if (!groupLocalFilename.equals(track.getLocalFilename())) {
1381 currentColorIndex++;
1382 if (currentColorIndex >= TRACK_COLOR_WHEEL.length) currentColorIndex = 0;
1383 currentY += trackHeight;
1384 groupLocalFilename = track.getLocalFilename();
1385 }
1386
1387 track.area = new Rectangle(
1388 (int) (x * width),
1389 (int) currentY,
1390 (int) (trackWidth * width),
1391 nTrackHeight
1392 );
1393
1394 // Have some give to become selectable / viewable
1395 if (track.area.width <= 1) {
1396
1397 track.area.width = 2;
1398 if ((track.area.x + 2) > width)
1399 track.area.x = width - 2;
1400 }
1401
1402 track.baseColor = TRACK_COLOR_WHEEL[currentColorIndex];
1403
1404 }
1405 }
1406
1407 /**
1408 * Sets the selection. Invalidates
1409 *
1410 * @param start
1411 * In frames. Clamped to be positive
1412 * @param length
1413 * In frames. If less or equal to one, then the selection length is one frame.
1414 * Otherwise selection is ranged.
1415 */
1416 public void setSelection(int start, int length) {
1417
1418 if (start < 0) start = 0;
1419 selectionStart = start;
1420 selectionLength = length;
1421
1422 invalidateSelf();
1423 }
1424
1425 private void setTrackState(String localFilename, int newState) {
1426 assert(localFilename != null);
1427
1428 for (Track track : tracks) {
1429 if (localFilename.equals(track.getLocalFilename())) {
1430 track.state = newState;
1431 }
1432 }
1433
1434 }
1435
1436 private void setTrackState(Track track, int newState) {
1437 track.state = newState;
1438 }
1439
1440 private static final int TRACKSTATE_READY = 1;
1441 private static final int TRACKSTATE_LOADING = 2;
1442 private static final int TRACKSTATE_FAILED = 3;
1443
1444
1445
1446 private void updateBorderColor() {
1447
1448 // Get border color currently used
1449 Color oldC = getSource().getBorderColor();
1450
1451 Color newC = TrackWidgetCommons.getBorderColor(
1452 SoundDesk.getInstance().isSolo(masterMix.getChannelID()),
1453 masterMix.isMuted());
1454
1455 // Update the color
1456 if (!newC.equals(oldC)) {
1457 setWidgetEdgeColor(newC);
1458 }
1459 }
1460
1461 /**
1462 * Updates the volume GUI for all views
1463 */
1464 public void updateVolume() {
1465 int volume = (int)(100 * masterMix.getVolume());
1466 if (playbackControlPopup != null)
1467 playbackControlPopup.updateVolume(volume);
1468
1469// if (soundDeskPopup != null)
1470// soundDeskPopup.updateVolume(volume);
1471 }
1472
1473
1474 /**
1475 * Updates the mute button GUI for all views.
1476 */
1477 public void updateMute() {
1478 if (playbackControlPopup != null)
1479 playbackControlPopup.updateMute(masterMix.isMuted());
1480
1481// if (soundDeskPopup != null)
1482// soundDeskPopup.updateMute(masterMix.isMuted());
1483 }
1484
1485
1486 /**
1487 * Updates the solo button GUI for all views.
1488 */
1489 public void updateSolo() {
1490 boolean isSolo = SoundDesk.getInstance().isSolo(masterMix.getChannelID());
1491 if (playbackControlPopup != null)
1492 playbackControlPopup.updateSolo(isSolo);
1493// if (soundDeskPopup != null)
1494// soundDeskPopup.updateSolo(isSolo);
1495
1496 }
1497
1498
1499 /**
1500 * The small popup for common actions.
1501 *
1502 * @author Brook Novak
1503 *
1504 */
1505 private class PlaybackPopup extends PlaybackControlPopup {
1506
1507 private static final long serialVersionUID = 1L;
1508
1509 public PlaybackPopup() {
1510 miscButton.setActionCommand("goto");
1511 miscButton.setIcon(IconRepository.getIcon("goto.png"));
1512 miscButton.setToolTipText("Goto linked frame");
1513 }
1514
1515 @Override
1516 public void onHide() {
1517 super.onHide();
1518 }
1519
1520 @Override
1521 public void onShow() {
1522 super.onShow();
1523 updateVolume((int)(100 * masterMix.getVolume()));
1524 updateMute(masterMix.isMuted());
1525 updateSolo(SoundDesk.getInstance().isSolo(masterMix.getChannelID()));
1526 }
1527
1528 public void actionPerformed(ActionEvent e) {
1529
1530 if (e.getSource() == miscButton) {
1531
1532 String absLink = LinkedTrack.this.getAbsoluteLink();
1533 if (absLink != null) {
1534 FrameUtils.DisplayFrame(absLink);
1535 }
1536
1537 } else {
1538
1539 // Relay shared action
1540 masterActionListener.actionPerformed(e);
1541
1542 }
1543 }
1544
1545 @Override
1546 protected void volumeChanged() {
1547 masterActionListener.volumeChanged(volumeSlider);
1548 }
1549
1550 @Override
1551 protected void muteChanged() {
1552 masterActionListener.muteChanged(muteButton);
1553 }
1554
1555 @Override
1556 protected void soloChanged() {
1557 masterActionListener.soloChanged(soloButton);
1558 }
1559
1560 }
1561
1562 /**
1563 * Common actions shared by the small playback popup and the sound desk popup.
1564 * Unlike the track widget these are managed within the linked widget since they
1565 * only are visible if the track widget is in view.
1566 *
1567 * @author Brook Novak
1568 */
1569 private class MasterControlActionListener implements ActionListener {
1570
1571 public void actionPerformed(ActionEvent e) {
1572
1573 if (!(state == PLAYING || state == STOPPED)) return;
1574
1575 assert(linkedOverdubbedFrame != null);
1576
1577 String actionCommand = (e.getActionCommand() == null) ? "" : e.getActionCommand();
1578
1579 if (actionCommand.equals("playpause")) {
1580
1581 if (!MultiTrackPlaybackController.getInstance().isPlaying(
1582 linkedOverdubbedFrame.getFrameName(), masterMix.getChannelID())) {
1583
1584 int startFrame = -1, endFrame = -1;
1585
1586 // Resume playback?
1587 if (MultiTrackPlaybackController.getInstance().isMarkedAsPaused(
1588 linkedOverdubbedFrame.getFrameName(), masterMix.getChannelID())) {
1589
1590 startFrame = MultiTrackPlaybackController.getInstance().getLastSuspendedFrame();
1591
1592 if (startFrame >= 0 && startFrame < totalFrameLength) {
1593
1594 assert(selectionStart >= 0);
1595
1596 // The user may have edited the audio track and reselected it
1597 // since the last pause. Thus select an appropriate end frame
1598 endFrame = (int)((selectionLength > 1) ?
1599 selectionStart + selectionLength : totalFrameLength - 1);
1600
1601 // Changed selection? it play range invalid?
1602 if (endFrame <= startFrame || startFrame < selectionStart) {
1603 startFrame = -1; // Play new selection (see below)
1604
1605 } else if (endFrame >= totalFrameLength) {
1606 endFrame = (int)totalFrameLength - 1;
1607 }
1608
1609 }
1610 }
1611
1612 // Play from beginning of selection to end of selection
1613 if (startFrame < 0) {
1614 startFrame = selectionStart;
1615 endFrame = (int)((selectionLength > 1) ?
1616 startFrame + selectionLength:
1617 totalFrameLength - 1);
1618 }
1619
1620 if (startFrame < endFrame) {
1621
1622 MultiTrackPlaybackController.getInstance().playFrame(
1623 LinkedTrack.this,
1624 linkedOverdubbedFrame.getFrameName(),
1625 masterMix.getChannelID(),
1626 false, // TODO: Set appropriatly
1627 0,
1628 startFrame,
1629 endFrame);
1630
1631 setState(PLAYBACK_LOADING);
1632
1633 }
1634
1635 } else { // pause
1636
1637 MultiTrackPlaybackController.getInstance().setPauseMark(true);
1638 MultiTrackPlaybackController.getInstance().stopPlayback();
1639
1640 }
1641
1642
1643 } else if (actionCommand.equals("stop")) {
1644
1645 if (MultiTrackPlaybackController.getInstance().isCurrentPlaybackSubject(
1646 linkedOverdubbedFrame.getFrameName(), masterMix.getChannelID())) {
1647 MultiTrackPlaybackController.getInstance().setPauseMark(false);
1648 MultiTrackPlaybackController.getInstance().stopPlayback();
1649 }
1650
1651
1652 } else if (actionCommand.equals("rewind")) {
1653 assert(state != PLAYING);
1654
1655 MultiTrackPlaybackController.getInstance().setPauseMark(false);
1656 currentPlaybackFramePosition = -1;
1657 setSelection(0, 0); // invalidates
1658 FrameGraphics.refresh(true);
1659
1660 }
1661 }
1662
1663 public void volumeChanged(JSlider volumeSlider) {
1664 masterMix.setVolume(((float)volumeSlider.getValue()) / 100.0f);
1665 }
1666
1667 public void muteChanged(JToggleButton muteButton) {
1668 masterMix.setMuted(muteButton.isSelected());
1669 }
1670
1671 public void soloChanged(JToggleButton soloButton) {
1672 SoundDesk.getInstance().setSoloIDPrefix(soloButton.isSelected() ?
1673 masterMix.getChannelID() : null
1674 );
1675 }
1676
1677 }
1678
1679 private class MouseActions implements MouseListener, MouseMotionListener {
1680
1681 public void mouseClicked(MouseEvent e) {
1682 assert(state != EMPTY_STATE);
1683
1684 if (state != PLAYING && state != STOPPED) return;
1685
1686 if (nameLabel.onMouseClicked(e)) {
1687 e.consume();
1688 return;
1689 }
1690
1691 }
1692
1693
1694 public void mouseEntered(MouseEvent e) {
1695 assert(state != EMPTY_STATE);
1696 }
1697
1698
1699 public void mouseExited(MouseEvent e) {
1700 assert(state != EMPTY_STATE);
1701
1702 }
1703
1704
1705 public void mousePressed(MouseEvent e) {
1706
1707 assert(state != EMPTY_STATE);
1708
1709 if (nameLabel.onMousePressed(e)) {
1710 e.consume();
1711 return;
1712 }
1713
1714 selectionStartX = e.getX();
1715
1716
1717 // Set selection start, and length as a single frame
1718 setSelection(frameAtX(selectionStartX), 1);
1719
1720 FrameGraphics.refresh(true);
1721 }
1722
1723
1724 public void mouseReleased(MouseEvent e) {
1725
1726
1727 assert(state != EMPTY_STATE);
1728
1729
1730 // Was the release a simple click
1731 if (selectionStartX == e.getX() && e.getButton() == MouseEvent.BUTTON1) {
1732
1733 // Traverse to the parent from of the selected track
1734 String link = null;
1735 for (Track track : tracks) {
1736 if (track.area.contains(e.getPoint())) {
1737 link = track.getParentFrameName();
1738 break;
1739 }
1740 }
1741
1742 if (link == null) link = LinkedTrack.this.getAbsoluteLink();
1743
1744 assert(link != null); // should be in an empty state otherwise
1745
1746 FrameUtils.DisplayFrame(link);
1747 }
1748
1749 if (nameLabel.onMouseReleased(e)) {
1750 e.consume();
1751 return;
1752 }
1753
1754
1755 }
1756
1757
1758 public void mouseDragged(MouseEvent e) {
1759
1760 assert(state != EMPTY_STATE);
1761
1762 if (state != PLAYING && state != STOPPED) return;
1763
1764 if (nameLabel.onMouseDragged(e)) {
1765 e.consume();
1766 return;
1767 }
1768
1769 if (selectionStartX < 0) return;
1770
1771 // Clamp mouse selection range
1772 int width = getWidth();
1773 int x = e.getX();
1774
1775 if (x > width) x = width;
1776 else if (x < 0) x = 0;
1777
1778 // Set selection range
1779 int frameAtCursor = frameAtX(x);
1780 int frameAtStartPoint = frameAtX(selectionStartX);
1781
1782 int start = Math.min(frameAtCursor, frameAtStartPoint);
1783 int length = Math.max(frameAtCursor, frameAtStartPoint) - start;
1784 if (length > 0) {
1785 setSelection(start, length);
1786 }
1787
1788 FrameGraphics.refresh(true);
1789 }
1790
1791 public void mouseMoved(MouseEvent e) {
1792 assert(state != EMPTY_STATE);
1793
1794 if (state != PLAYING && state != STOPPED) return;
1795
1796 if (nameLabel.onMouseMoved(e, _swingComponent)) {
1797 e.consume();
1798 return;
1799 }
1800
1801 if (playbackControlPopup == null) {
1802 playbackControlPopup = new PlaybackPopup();
1803 }
1804
1805 // Only show popup iff there is nothing expanded / expanding.
1806 if (!PopupManager.getInstance().isShowing(playbackControlPopup) &&
1807 !ExpandedTrackManager.getInstance().doesExpandedTrackExist()) {
1808
1809 // Get rid of all popups
1810 PopupManager.getInstance().hideAutohidePopups();
1811
1812 Rectangle animationSource = _swingComponent.getBounds();
1813
1814 // Determine where popup should show
1815 int x = LinkedTrack.this.getX();
1816 int y = LinkedTrack.this.getY() - playbackControlPopup.getHeight() - 2; // by default show above
1817
1818 // I get sick.dizzy from the popup expanding from the whole thing...
1819 animationSource.height = 1;
1820 animationSource.width = Math.min(animationSource.width, playbackControlPopup.getWidth());
1821
1822 if (y < 0) {
1823 y = LinkedTrack.this.getY() + LinkedTrack.this.getHeight() + 2;
1824 animationSource.y = y - 2;
1825 }
1826
1827 // Animate the popup
1828 PopupManager.getInstance().showPopup(
1829 playbackControlPopup,
1830 new Point(x, y),
1831 _swingComponent,
1832 PopupManager.getInstance().new ExpandShrinkAnimator(
1833 animationSource,
1834 Color.LIGHT_GRAY));
1835
1836 PopupReaper.getInstance().initPopupLifetime(
1837 playbackControlPopup,
1838 PopupManager.getInstance().new ExpandShrinkAnimator(
1839 animationSource,
1840 Color.LIGHT_GRAY),
1841 TrackWidgetCommons.POPUP_LIFETIME);
1842
1843 } else {
1844 PopupReaper.getInstance().revivePopup(playbackControlPopup, TrackWidgetCommons.POPUP_LIFETIME);
1845 }
1846
1847
1848 // Handle selection / invalidation of mouse hovering over track segments
1849 boolean isOneSelected = false;
1850 boolean shouldInvalidate = false;
1851 for (Track track : tracks) {
1852
1853 if (isOneSelected && track.isSelected) {
1854 track.isSelected = false;
1855 shouldInvalidate = true;
1856 } else if (!isOneSelected) {
1857 isOneSelected = track.area.contains(e.getPoint());
1858 shouldInvalidate |= (isOneSelected != track.isSelected);
1859 track.isSelected = isOneSelected;
1860 }
1861
1862 }
1863
1864 if (shouldInvalidate) {
1865 invalidateSelf();
1866 FrameGraphics.refresh(true);
1867 }
1868 }
1869 }
1870
1871
1872 /**
1873 * A wrapper for {@link AbsoluteTrackNode} - including graphical representation data.
1874 * @author Brook Novak
1875 */
1876 private class Track {
1877
1878 private AbsoluteTrackNode absNode; // immutable. Never null
1879
1880 private Rectangle area;
1881
1882 private Color baseColor;
1883
1884 boolean isSelected = false;
1885
1886 private int state = TRACKSTATE_READY;
1887
1888 Track(AbsoluteTrackNode absNode) {
1889 assert(absNode != null);
1890 this.absNode = absNode;
1891
1892 // This is a shame... but due to time restictrictions must limit the system to
1893 // only point to the local mix. I think its a bit overkill as this stage :(
1894 SoundDesk.getInstance().setUseLocalTrackMix(absNode.getChannelID(), true);
1895
1896
1897 }
1898
1899 public String getChannelID() {
1900 return absNode.getChannelID();
1901 }
1902
1903 public long getABSStartTime() {
1904 return absNode.getABSStartTime();
1905 }
1906
1907 public String getLocalFilename() {
1908 return absNode.getTrackNode().getLocalFilename();
1909 }
1910
1911 public long getRunningTime() {
1912 return absNode.getTrackNode().getRunningTime();
1913 }
1914
1915 public String getParentFrameName() {
1916 return absNode.getParentFrameName();
1917 }
1918
1919 public String getTrackName() {
1920 return absNode.getTrackNode().getName();
1921 }
1922
1923 public void paintTrackArea(Graphics2D g, int x, int y) {
1924
1925 if (state == TRACKSTATE_LOADING && LinkedTrack.this.state == PLAYBACK_LOADING) {
1926 g.setColor(TRACK_LOAD_COLOR);
1927 } else if (state == TRACKSTATE_FAILED) {
1928 g.setColor(TRACK_FAIL_COLOR);
1929 } else {
1930 g.setColor(baseColor);
1931 }
1932
1933 Paint restore = g.getPaint();
1934 if (ApolloSystem.useQualityGraphics) {
1935
1936 GradientPaint gp = new GradientPaint(
1937 (int) (area.x + x + (area.width / 2)), area.y + y + (int)(area.height * 0.8), baseColor,
1938 (int) (area.x + x + (area.width / 2)), area.y + y, TRACK_SELECTED_BORDER_COLOR);
1939 g.setPaint(gp);
1940 }
1941
1942 // Draw a filled rect - represented a track in the heirarchy
1943 g.fillRect(area.x + x, area.y + y, area.width, area.height);
1944
1945 if (ApolloSystem.useQualityGraphics) {
1946 g.setPaint(restore);
1947 }
1948
1949 }
1950
1951 public void paintTrackBorder(Graphics g, int x, int y) {
1952
1953 if (isSelected) {
1954 g.setColor(TRACK_SELECTED_BORDER_COLOR);
1955 ((Graphics2D)g).setStroke(TRACK_SELECTED_BORDER);
1956 } else {
1957 g.setColor(TRACK_BORDER_COLOR);
1958 ((Graphics2D)g).setStroke(TRACK_BORDER); // todo muted colors etc...
1959 }
1960
1961 g.drawRect(area.x + x, area.y + y, area.width, area.height);
1962 }
1963
1964 }
1965
1966 @Override
1967 public boolean isWidgetEdgeThicknessAdjustable() {
1968 return false;
1969 }
1970
1971
1972
1973}
Note: See TracBrowser for help on using the repository browser.