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

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

Refactored, improved gui, now always used a cross platform LAF for apollo. Added basic settings on defualt frame

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