source: trunk/src/org/apollo/widgets/LinkedTrack.java@ 1102

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

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

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