org.datavyu.controllers.component.MixerController.java Source code

Java tutorial

Introduction

Here is the source code for org.datavyu.controllers.component.MixerController.java

Source

/**
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.datavyu.controllers.component;

import java.awt.Adjustable;
import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.AdjustmentEvent;
import java.awt.event.AdjustmentListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;

import java.util.EventObject;
import java.util.List;
import java.util.Map;

import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JLayeredPane;
import javax.swing.JPanel;
import javax.swing.JScrollBar;
import javax.swing.JScrollPane;
import javax.swing.JSlider;
import javax.swing.JToggleButton;
import javax.swing.ScrollPaneConstants;
import javax.swing.SwingUtilities;
import javax.swing.Box.Filler;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.EventListenerList;

import net.miginfocom.swing.MigLayout;

import org.datavyu.Datavyu;

import org.datavyu.event.component.CarriageEvent;
import org.datavyu.event.component.CarriageEvent.EventType;
import org.datavyu.event.component.CarriageEventListener;
import org.datavyu.event.component.TimescaleEvent;
import org.datavyu.event.component.TimescaleListener;
import org.datavyu.event.component.TracksControllerEvent;
import org.datavyu.event.component.TracksControllerEvent.TracksEvent;
import org.datavyu.event.component.TracksControllerListener;

import org.datavyu.models.component.MixerConstants;
import org.datavyu.models.component.MixerModelImpl;
import org.datavyu.models.component.MixerModel;
import org.datavyu.models.component.NeedleConstants;
import org.datavyu.models.component.RegionModel;
import org.datavyu.models.component.RegionState;
import org.datavyu.models.component.RegionConstants;
import org.datavyu.models.component.TimescaleConstants;
import org.datavyu.models.component.TrackConstants;
import org.datavyu.models.component.TrackModel;
import org.datavyu.models.component.ViewportModel;
import org.datavyu.models.component.ViewportState;
import org.datavyu.models.id.Identifier;

import org.datavyu.views.component.TrackPainter;

import com.google.common.collect.Maps;

import org.apache.commons.lang.text.StrSubstitutor;

import com.apple.eawt.event.GesturePhaseEvent;
import com.apple.eawt.event.GesturePhaseListener;
import com.apple.eawt.event.GestureUtilities;
import com.apple.eawt.event.MagnificationEvent;
import com.apple.eawt.event.MagnificationListener;
import com.apple.eawt.event.SwipeEvent;
import com.apple.eawt.event.SwipeListener;

import com.sun.jna.Platform;

import org.datavyu.plugins.CustomActions;

/**
 * This class manages the tracks information interface.
 */
public final class MixerController
        implements PropertyChangeListener, CarriageEventListener, AdjustmentListener, TimescaleListener {

    /** Root interface panel. */
    private JPanel tracksPanel;

    /** Scroll pane that holds track information. */
    private JScrollPane tracksScrollPane;

    /** This layered pane holds the needle painter. */
    private JLayeredPane layeredPane;

    /**
     * Zoom setting in the interval (0, 1.0) where increasing values represent "zooming in".
     */
    private double zoomSetting = MixerConstants.DEFAULT_ZOOM;

    private final int TRACKS_SCROLL_BAR_RANGE = 1000000;

    /** The value of the earliest video's start time in milliseconds. */
    private long minStart;

    /** Listeners interested in tracks controller events. */
    private EventListenerList listenerList;

    /** Controller responsible for managing the time scale. */
    private TimescaleController timescaleController;

    /** Controller responsible for managing the timing needle. */
    private NeedleController needleController;

    /** Controller responsible for managing a selected region. */
    private RegionController regionController;

    /** Controller responsible for managing tracks. */
    private TracksEditorController tracksEditorController;

    /** Bookmark (create snap point) button. */
    private JButton bookmarkButton;

    /** Button for locking and unlocking all tracks. */
    private JToggleButton lockToggle;

    /** Tracks horizontal scroll bar. */
    private JScrollBar tracksScrollBar;
    private boolean isUpdatingTracksScrollBar = false;

    /** Zoom slider. */
    private JSlider zoomSlide;
    private boolean isUpdatingZoomSlide = false;

    /** Zoom icon. */
    private final ImageIcon zoomIcon = new ImageIcon(getClass().getResource("/icons/magnifier.png"));

    /** Master mixer to listen to. */
    private final MixerModelImpl mixerModel;
    private final ViewportModel viewportModel;
    private final RegionModel regionModel;

    /** Listens and processes gestures on Mac OS X. */
    private final OSXGestureListener osxGestureListener = Platform.isMac() ? new OSXGestureListener() : null;

    /**
     * Create a new MixerController.
     */
    public MixerController() {
        mixerModel = new MixerModelImpl();

        viewportModel = mixerModel.getViewportModel();
        viewportModel.addPropertyChangeListener(this);

        regionModel = mixerModel.getRegionModel();
        regionModel.addPropertyChangeListener(this);

        runInEDT(new Runnable() {
            @Override
            public void run() {
                initView();
            }
        });
    }

    private static void runInEDT(final Runnable task) {

        if (SwingUtilities.isEventDispatchThread()) {
            task.run();
        } else {
            SwingUtilities.invokeLater(task);
        }
    }

    private void initView() {

        // Set default scale values
        minStart = 0;

        listenerList = new EventListenerList();

        // Set up the root panel
        tracksPanel = new JPanel();
        tracksPanel.setLayout(new MigLayout("ins 0", "[left|left|left|left]rel push[right|right]", ""));
        tracksPanel.setBackground(Color.WHITE);

        if (Platform.isMac()) {
            osxGestureListener.register(tracksPanel);
        }

        // Menu buttons
        lockToggle = new JToggleButton("Lock all");
        lockToggle.addActionListener(new ActionListener() {
            public void actionPerformed(final ActionEvent e) {
                lockToggleHandler(e);
            }
        });
        lockToggle.setName("lockToggleButton");

        bookmarkButton = new JButton("Add Bookmark");
        bookmarkButton.addActionListener(new ActionListener() {
            public void actionPerformed(final ActionEvent e) {
                addBookmarkHandler();
            }
        });
        bookmarkButton.setEnabled(false);
        bookmarkButton.setName("bookmarkButton");

        JButton snapRegion = new JButton("Snap Region");
        snapRegion.addActionListener(new ActionListener() {
            public void actionPerformed(final ActionEvent e) {
                snapRegionHandler(e);
            }
        });
        snapRegion.setName("snapRegionButton");

        JButton clearRegion = new JButton("Clear Region");
        clearRegion.addActionListener(new ActionListener() {
            public void actionPerformed(final ActionEvent e) {
                clearRegionHandler(e);
            }
        });
        clearRegion.setName("clearRegionButton");

        zoomSlide = new JSlider(JSlider.HORIZONTAL, 1, 1000, 1);
        zoomSlide.addChangeListener(new ChangeListener() {
            public void stateChanged(final ChangeEvent e) {

                if (!isUpdatingZoomSlide && zoomSlide.getValueIsAdjusting()) {

                    try {
                        isUpdatingZoomSlide = true;
                        zoomSetting = (double) (zoomSlide.getValue() - zoomSlide.getMinimum())
                                / (zoomSlide.getMaximum() - zoomSlide.getMinimum() + 1);
                        viewportModel.setViewportZoom(zoomSetting,
                                needleController.getNeedleModel().getCurrentTime());
                    } finally {
                        isUpdatingZoomSlide = false;
                    }
                }
            }
        });
        zoomSlide.setName("zoomSlider");
        zoomSlide.setBackground(tracksPanel.getBackground());

        JButton zoomRegionButton = new JButton("", zoomIcon);
        zoomRegionButton.addActionListener(new ActionListener() {
            public void actionPerformed(final ActionEvent e) {
                zoomToRegion(e);
            }
        });
        zoomRegionButton.setName("zoomRegionButton");

        tracksPanel.add(lockToggle);
        tracksPanel.add(bookmarkButton);
        tracksPanel.add(snapRegion);
        tracksPanel.add(clearRegion);
        tracksPanel.add(zoomRegionButton);
        tracksPanel.add(zoomSlide, "wrap");

        timescaleController = new TimescaleController(mixerModel);
        timescaleController.addTimescaleEventListener(this);
        needleController = new NeedleController(this, mixerModel);
        regionController = new RegionController(mixerModel);
        tracksEditorController = new TracksEditorController(this, mixerModel);

        needleController.setTimescaleTransitionHeight(
                timescaleController.getTimescaleModel().getZoomWindowToTrackTransitionHeight());
        needleController
                .setZoomIndicatorHeight(timescaleController.getTimescaleModel().getZoomWindowIndicatorHeight());

        // Set up the layered pane
        layeredPane = new JLayeredPane();
        layeredPane.setLayout(new MigLayout("fillx, ins 0"));

        final int layeredPaneHeight = 272;
        final int timescaleViewHeight = timescaleController.getTimescaleModel().getHeight();

        final int needleHeadHeight = (int) Math.ceil(NeedleConstants.NEEDLE_HEAD_HEIGHT);
        final int tracksScrollPaneY = needleHeadHeight + 1;
        final int timescaleViewY = layeredPaneHeight - MixerConstants.HSCROLL_HEIGHT - timescaleViewHeight;
        final int tracksScrollPaneHeight = timescaleViewY - tracksScrollPaneY;
        final int tracksScrollBarY = timescaleViewY + timescaleViewHeight;
        final int needleAndRegionMarkerHeight = (timescaleViewY + timescaleViewHeight
                - timescaleController.getTimescaleModel().getZoomWindowIndicatorHeight()
                - timescaleController.getTimescaleModel().getZoomWindowToTrackTransitionHeight() + 1);

        // Set up filler component responsible for horizontal resizing of the
        // layout.
        {

            // Null args; let layout manager handle sizes.
            Box.Filler filler = new Filler(null, null, null);
            filler.setName("Filler");
            filler.addComponentListener(new SizeHandler());

            Map<String, String> constraints = Maps.newHashMap();
            constraints.put("wmin", Integer.toString(MixerConstants.MIXER_MIN_WIDTH));

            // TODO Could probably use this same component to handle vertical
            // resizing...
            String template = "id filler, h 0!, grow 100 0, wmin ${wmin}, cell 0 0 ";
            StrSubstitutor sub = new StrSubstitutor(constraints);

            layeredPane.setLayer(filler, MixerConstants.FILLER_ZORDER);
            layeredPane.add(filler, sub.replace(template), MixerConstants.FILLER_ZORDER);
        }

        // Set up the timescale layout
        {
            JComponent timescaleView = timescaleController.getView();

            Map<String, String> constraints = Maps.newHashMap();
            constraints.put("x", Integer.toString(TimescaleConstants.XPOS_ABS));
            constraints.put("y", Integer.toString(timescaleViewY));

            // Calculate padding from the right
            int rightPad = (int) (RegionConstants.RMARKER_WIDTH + MixerConstants.VSCROLL_WIDTH
                    + MixerConstants.R_EDGE_PAD);
            constraints.put("x2", "(filler.w-" + rightPad + ")");
            constraints.put("y2", "(tscale.y+${height})");
            constraints.put("height", Integer.toString(timescaleViewHeight));

            String template = "id tscale, pos ${x} ${y} ${x2} ${y2}";
            StrSubstitutor sub = new StrSubstitutor(constraints);

            // Must call setLayer first.
            layeredPane.setLayer(timescaleView, MixerConstants.TIMESCALE_ZORDER);
            layeredPane.add(timescaleView, sub.replace(template), MixerConstants.TIMESCALE_ZORDER);
        }

        // Set up the scroll pane's layout.
        {
            tracksScrollPane = new JScrollPane(tracksEditorController.getView());
            tracksScrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
            tracksScrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
            tracksScrollPane.setBorder(BorderFactory.createEmptyBorder());
            tracksScrollPane.setName("jScrollPane");

            Map<String, String> constraints = Maps.newHashMap();
            constraints.put("x", "0");
            constraints.put("y", Integer.toString(tracksScrollPaneY));
            constraints.put("x2", "(filler.w-" + MixerConstants.R_EDGE_PAD + ")");
            constraints.put("height", Integer.toString(tracksScrollPaneHeight));

            String template = "pos ${x} ${y} ${x2} n, h ${height}!";
            StrSubstitutor sub = new StrSubstitutor(constraints);

            layeredPane.setLayer(tracksScrollPane, MixerConstants.TRACKS_ZORDER);
            layeredPane.add(tracksScrollPane, sub.replace(template), MixerConstants.TRACKS_ZORDER);
        }

        // Create the region markers and set up the layout.
        {
            JComponent regionView = regionController.getView();

            Map<String, String> constraints = Maps.newHashMap();

            int x = (int) (TrackConstants.HEADER_WIDTH - RegionConstants.RMARKER_WIDTH);
            constraints.put("x", Integer.toString(x));
            constraints.put("y", "0");

            // Padding from the right
            int rightPad = MixerConstants.R_EDGE_PAD + MixerConstants.VSCROLL_WIDTH - 2;

            constraints.put("x2", "(filler.w-" + rightPad + ")");
            constraints.put("height", Integer.toString(needleAndRegionMarkerHeight));

            String template = "pos ${x} ${y} ${x2} n, h ${height}::";
            StrSubstitutor sub = new StrSubstitutor(constraints);

            layeredPane.setLayer(regionView, MixerConstants.REGION_ZORDER);
            layeredPane.add(regionView, sub.replace(template), MixerConstants.REGION_ZORDER);
        }

        // Set up the timing needle's layout
        {
            JComponent needleView = needleController.getView();

            Map<String, String> constraints = Maps.newHashMap();

            int x = (int) (TrackConstants.HEADER_WIDTH - NeedleConstants.NEEDLE_HEAD_WIDTH
                    + NeedleConstants.NEEDLE_WIDTH);
            constraints.put("x", Integer.toString(x));
            constraints.put("y", "0");

            // Padding from the right
            int rightPad = MixerConstants.R_EDGE_PAD + MixerConstants.VSCROLL_WIDTH - 1;

            constraints.put("x2", "(filler.w-" + rightPad + ")");
            constraints.put("height",
                    Integer.toString(needleAndRegionMarkerHeight
                            + timescaleController.getTimescaleModel().getZoomWindowToTrackTransitionHeight()
                            + timescaleController.getTimescaleModel().getZoomWindowIndicatorHeight() - 1));

            String template = "pos ${x} ${y} ${x2} n, h ${height}::";
            StrSubstitutor sub = new StrSubstitutor(constraints);

            layeredPane.setLayer(needleView, MixerConstants.NEEDLE_ZORDER);
            layeredPane.add(needleView, sub.replace(template), MixerConstants.NEEDLE_ZORDER);
        }

        // Set up the snap marker's layout
        {
            JComponent markerView = tracksEditorController.getMarkerView();

            Map<String, String> constraints = Maps.newHashMap();
            constraints.put("x", Integer.toString(TimescaleConstants.XPOS_ABS));
            constraints.put("y", Integer.toString(needleHeadHeight + 1));

            // Padding from the right
            int rightPad = MixerConstants.R_EDGE_PAD + MixerConstants.VSCROLL_WIDTH - 1;

            constraints.put("x2", "(filler.w-" + rightPad + ")");
            constraints.put("height", Integer.toString(needleAndRegionMarkerHeight - needleHeadHeight - 1));

            String template = "pos ${x} ${y} ${x2} n, h ${height}::";
            StrSubstitutor sub = new StrSubstitutor(constraints);

            layeredPane.setLayer(markerView, MixerConstants.MARKER_ZORDER);
            layeredPane.add(markerView, sub.replace(template), MixerConstants.MARKER_ZORDER);
        }

        // Set up the tracks horizontal scroll bar
        {
            tracksScrollBar = new JScrollBar(Adjustable.HORIZONTAL);
            tracksScrollBar.setValues(0, TRACKS_SCROLL_BAR_RANGE, 0, TRACKS_SCROLL_BAR_RANGE);
            tracksScrollBar.setUnitIncrement(TRACKS_SCROLL_BAR_RANGE / 20);
            tracksScrollBar.setBlockIncrement(TRACKS_SCROLL_BAR_RANGE / 2);
            tracksScrollBar.addAdjustmentListener(this);
            tracksScrollBar.setValueIsAdjusting(false);
            tracksScrollBar.setVisible(false);
            tracksScrollBar.setName("horizontalScrollBar");

            Map<String, String> constraints = Maps.newHashMap();
            constraints.put("x", Integer.toString(TimescaleConstants.XPOS_ABS));
            constraints.put("y", Integer.toString(tracksScrollBarY));

            int rightPad = (int) (RegionConstants.RMARKER_WIDTH + MixerConstants.VSCROLL_WIDTH
                    + MixerConstants.R_EDGE_PAD);
            constraints.put("x2", "(filler.w-" + rightPad + ")");
            constraints.put("height", Integer.toString(MixerConstants.HSCROLL_HEIGHT));

            String template = "pos ${x} ${y} ${x2} n, h ${height}::";
            StrSubstitutor sub = new StrSubstitutor(constraints);

            layeredPane.setLayer(tracksScrollBar, MixerConstants.TRACKS_SB_ZORDER);
            layeredPane.add(tracksScrollBar, sub.replace(template), MixerConstants.TRACKS_SB_ZORDER);
        }

        {
            Map<String, String> constraints = Maps.newHashMap();
            constraints.put("span", "6");
            constraints.put("width", Integer.toString(MixerConstants.MIXER_MIN_WIDTH));
            constraints.put("height", Integer.toString(layeredPaneHeight));

            String template = "growx, span ${span}, w ${width}::, h ${height}::, wrap";
            StrSubstitutor sub = new StrSubstitutor(constraints);

            tracksPanel.add(layeredPane, sub.replace(template));
        }

        tracksPanel.validate();
    }

    /**
     * @return the panel containing the tracks interface.
     */
    public JPanel getTracksPanel() {
        return tracksPanel;
    }

    /**
     * Sets the longest data feed duration.
     *
     * @param newMaxEnd
     *            duration in milliseconds
     */
    public void setMaxEnd(final long newMaxEnd, final boolean resetViewportWindow) {
        viewportModel.setViewportMaxEnd(newMaxEnd, resetViewportWindow);

        if (resetViewportWindow) {
            regionModel.resetPlaybackRegion();
        }
    }

    /**
     * Add a new track to the interface.
     *
     * @param id
     *            Identifier of the track.
     * @param icon
     *            Icon associated with the track.
     * @param mediaPath
     *            Absolute path to the media file.
     * @param trackName
     *            Name of the track.
     * @param duration
     *            The total duration of the track in milliseconds.
     * @param offset
     *            The amount of playback offset in milliseconds.
     * @param trackPainter
     *            The track painter to use.
     */
    public void addNewTrack(final Identifier id, final ImageIcon icon, final String mediaPath,
            final String trackName, final long duration, final long offset, final TrackPainter trackPainter) {

        // Check if the scale needs to be updated.
        final long trackEnd = duration + offset;
        final ViewportState viewport = viewportModel.getViewport();

        if ((trackEnd > viewport.getMaxEnd())
                || ((tracksEditorController.numberOfTracks() == 0) && (trackEnd > 0))) {
            viewportModel.setViewportMaxEnd(trackEnd, true);
            regionModel.resetPlaybackRegion();
        }

        tracksEditorController.addNewTrack(id, icon, trackName, mediaPath, duration, offset, this, trackPainter);
        tracksScrollPane.validate();

        updateGlobalLockToggle();
    }

    /** Clears the region of interest and zooms all the way out. */
    public void clearRegionAndZoomOut() {
        clearRegionOfInterest();
        zoomToRegion(null);
    }

    /**
     * Bind track actions to a data viewer.
     *
     * @param trackId
     *            Identifier of the track
     * @param actions
     *            Actions to bind with
     */
    public void bindTrackActions(final Identifier trackId, final CustomActions actions) {

        if (actions == null) {
            return;
        }

        runInEDT(new Runnable() {
            @Override
            public void run() {
                tracksEditorController.bindTrackActions(trackId, actions);
            }
        });
    }

    /**
     * Used to set up the track interface.
     *
     * @param trackId
     *            Track identifier.
     * @param bookmark
     *            Bookmark position in milliseconds.
     * @param lock
     *            True if track movement is locked, false otherwise.
     */
    public void setTrackInterfaceSettings(final Identifier trackId, final List<Long> bookmarks,
            final boolean lock) {
        runInEDT(new Runnable() {
            @Override
            public void run() {
                tracksEditorController.setBookmarkPositions(trackId, bookmarks);
                tracksEditorController.setMovementLock(trackId, lock);
            }
        });
    }

    /**
     * For backwards compatibility; used the set up the track interface. If
     * there are multiple tracks identified by the same media path, only the
     * first track found is used.
     *
     * @param mediaPath
     *            Absolute path to the media file.
     * @param bookmark
     *            Bookmark position in milliseconds.
     * @param lock
     *            True if track movement is locked, false otherwise.
     */
    @Deprecated
    public void setTrackInterfaceSettings(final String mediaPath, final List<Long> bookmarks, final boolean lock) {
        runInEDT(new Runnable() {
            @Override
            public void run() {
                tracksEditorController.setBookmarkPositions(mediaPath, bookmarks);
                tracksEditorController.setMovementLock(mediaPath, lock);
            }
        });
    }

    /**
     * Zooms into the displayed region and re-adjusts the timing needle
     * accordingly.
     *
     * @param evt
     */
    public void zoomToRegion(final ActionEvent evt) {
        final ViewportState viewport = viewportModel.getViewport();
        final RegionState region = regionModel.getRegion();

        if (region.getRegionDuration() >= 1) {
            final int percentOfRegionToPadOutsideMarkers = 5;
            assert (percentOfRegionToPadOutsideMarkers >= 0) && (percentOfRegionToPadOutsideMarkers <= 100);

            final long displayedAreaStart = Math.max(region.getRegionStart()
                    - (region.getRegionDuration() * percentOfRegionToPadOutsideMarkers / 100), 0);
            final long displayedAreaEnd = Math.min(
                    region.getRegionEnd() + (region.getRegionDuration() * percentOfRegionToPadOutsideMarkers / 100),
                    viewport.getMaxEnd());

            viewportModel.setViewportWindow(displayedAreaStart, displayedAreaEnd);
            needleController.setCurrentTime(region.getRegionStart());
        }
    }

    /**
     * Remove from track panel.
     *
     * @param trackId
     *            identifier of the track to remove.
     */
    public void deregisterTrack(final Identifier trackId) {
        tracksEditorController.removeTrack(trackId, this);

        // Update tracks panel display
        tracksScrollPane.validate();

        updateGlobalLockToggle();
    }

    /**
     * Removes all track components from this controller and resets components.
     */
    public void removeAll() {
        tracksEditorController.removeAllTracks();

        viewportModel.resetViewport();
        regionModel.resetPlaybackRegion();
        needleController.setCurrentTime(0);

        tracksScrollPane.validate();

        tracksPanel.validate();
        tracksPanel.repaint();

        updateGlobalLockToggle();
    }

    /**
     * @return all track models used to represent the UI.
     */
    public Iterable<TrackModel> getAllTrackModels() {
        return tracksEditorController.getAllTrackModels();
    }

    public TrackModel getTrackModel(final Identifier id) {
        return tracksEditorController.getTrackModel(id);
    }

    public MixerModel getMixerModel() {
        return mixerModel;
    }

    /**
     * @return NeedleController.
     */
    public NeedleController getNeedleController() {
        return needleController;
    }

    /**
     * @return RegionController.
     */
    public RegionController getRegionController() {
        return regionController;
    }

    /**
     * @return TimescaleController.
     */
    public TimescaleController getTimescaleController() {
        return timescaleController;
    }

    /**
     * @return TracksEditorController.
     */
    public TracksEditorController getTracksEditorController() {
        return tracksEditorController;
    }

    private void updateZoomSlide(final ViewportState viewport) {
        assert SwingUtilities.isEventDispatchThread();

        if (isUpdatingZoomSlide) {
            return;
        }

        try {
            isUpdatingZoomSlide = true;

            zoomSlide.setValue((int) Math
                    .round((viewport.getZoomLevel() * (zoomSlide.getMaximum() - zoomSlide.getMinimum() + 1))
                            + zoomSlide.getMinimum()));
        } finally {
            isUpdatingZoomSlide = false;
        }
    }

    /**
     * Update scroll bar values.
     */
    private void updateTracksScrollBar(final ViewportState viewport) {
        assert SwingUtilities.isEventDispatchThread();

        if (isUpdatingTracksScrollBar) {
            return;
        }

        try {
            isUpdatingTracksScrollBar = true;

            final int startValue = (int) Math
                    .round((double) viewport.getViewStart() * TRACKS_SCROLL_BAR_RANGE / viewport.getMaxEnd());
            final int extentValue = (int) Math
                    .round((double) (viewport.getViewDuration()) * TRACKS_SCROLL_BAR_RANGE / viewport.getMaxEnd());

            tracksScrollBar.setValues(startValue, extentValue, 0, TRACKS_SCROLL_BAR_RANGE);
            tracksScrollBar.setUnitIncrement(extentValue / 20);
            tracksScrollBar.setBlockIncrement(extentValue / 2);
            tracksScrollBar.setVisible((viewport.getViewDuration()) < viewport.getMaxEnd());
        } finally {
            isUpdatingTracksScrollBar = false;
            tracksPanel.validate();
        }
    }

    /**
     * Handles the event for adding a temporal bookmark to selected tracks.
     */
    private void addBookmarkHandler() {
        tracksEditorController.addTemporalBookmarkToSelected(needleController.getNeedleModel().getCurrentTime());
    }

    /**
     * Handles the event for toggling the snap functionality on and off.
     *
     * @param e
     *            expecting the event to be generated from a JToggleButton
     */
    private void snapRegionHandler(final ActionEvent e) {
        Datavyu.getDataController().setRegionOfInterestAction();

    }

    /**
     * Handles the event for clearing the snap region.
     *
     * @param e The event that triggered this action.
     */
    private void clearRegionHandler(final ActionEvent e) {
        clearRegionOfInterest();
    }

    /**
     * Clears the region of interest.
     */
    public void clearRegionOfInterest() {
        regionModel.resetPlaybackRegion();
    }

    /**
     * Handles the event for toggling movement of tracks on and off.
     *
     * @param e the event to handle
     */
    private void lockToggleHandler(final ActionEvent e) {
        JToggleButton toggle = (JToggleButton) e.getSource();
        tracksEditorController.setLockedState(toggle.isSelected());
        updateGlobalLockToggle();
    }

    /**
     * Handles the event for scrolling the tracks interface horizontally.
     *
     * @param e the event to handle
     */
    public void adjustmentValueChanged(final AdjustmentEvent e) {

        if (isUpdatingTracksScrollBar) {
            return;
        }

        final ViewportState viewport = viewportModel.getViewport();

        final int startValue = tracksScrollBar.getValue();

        assert tracksScrollBar.getMinimum() == 0;

        final long newWindowStart = (long) Math
                .round((double) startValue / tracksScrollBar.getMaximum() * viewport.getMaxEnd());
        final long newWindowEnd = newWindowStart + viewport.getViewDuration() - 1;

        viewportModel.setViewportWindow(newWindowStart, newWindowEnd);

        tracksPanel.repaint();
    }

    /**
     * TrackPainter recorded a change in the track's offset using the mouse.
     *
     * @param e the event to handle
     */
    public void offsetChanged(final CarriageEvent e) {
        final boolean wasOffsetChanged = tracksEditorController.setTrackOffset(e.getTrackId(), e.getOffset(),
                e.getTemporalPosition());
        final CarriageEvent newEvent;

        if (wasOffsetChanged) {
            final long newOffset = tracksEditorController.getTrackModel(e.getTrackId()).getOffset();
            newEvent = new CarriageEvent(e.getSource(), e.getTrackId(), newOffset, e.getBookmarks(),
                    e.getDuration(), e.getTemporalPosition(), e.getEventType(), e.hasModifiers());
        } else {
            newEvent = e;
        }

        fireTracksControllerEvent(TracksEvent.CARRIAGE_EVENT, newEvent);
        tracksPanel.invalidate();
        tracksPanel.repaint();
    }

    /**
     * Track is requesting current temporal position to create a bookmark.
     *
     * @param e the event to handle
     */
    public void requestBookmark(final CarriageEvent e) {
        TrackController trackController = (TrackController) e.getSource();
        trackController.addTemporalBookmark(needleController.getNeedleModel().getCurrentTime());

        CarriageEvent newEvent = new CarriageEvent(e.getSource(), e.getTrackId(), e.getOffset(),
                trackController.getBookmarks(), e.getDuration(), e.getTemporalPosition(),
                EventType.BOOKMARK_CHANGED, e.hasModifiers());

        fireTracksControllerEvent(TracksEvent.CARRIAGE_EVENT, newEvent);
    }

    /**
     * Track is requesting for bookmark to be saved.
     *
     * @param e the event to handle
     */
    public void saveBookmark(final CarriageEvent e) {
        fireTracksControllerEvent(TracksEvent.CARRIAGE_EVENT, e);
    }

    /**
     * Track lock state changed.
     *
     * @param e the event to handle
     */
    public void lockStateChanged(final CarriageEvent e) {
        fireTracksControllerEvent(TracksEvent.CARRIAGE_EVENT, e);
        updateGlobalLockToggle();
    }

    /**
     * A track's selection state was changed.
     *
     * @param e the event to handle
     */
    public void selectionChanged(final CarriageEvent e) {
        bookmarkButton.setEnabled(tracksEditorController.hasSelectedTracks());
    }

    public void jumpToTime(final TimescaleEvent e) {
        fireTracksControllerEvent(TracksEvent.TIMESCALE_EVENT, e);
    }

    /**
     * Register listeners who are interested in events from this class.
     *
     * @param listener the listener to register
     */
    public void addTracksControllerListener(final TracksControllerListener listener) {

        synchronized (this) {
            listenerList.add(TracksControllerListener.class, listener);
        }
    }

    /**
     * De-register listeners from receiving events from this class.
     *
     * @param listener the listener to remove
     */
    public void removeTracksControllerListener(final TracksControllerListener listener) {

        synchronized (this) {
            listenerList.remove(TracksControllerListener.class, listener);
        }
    }

    private void updateGlobalLockToggle() {

        if (tracksEditorController.isAnyTrackUnlocked()) {
            lockToggle.setSelected(false);
            lockToggle.setText("Lock all");
        } else {
            lockToggle.setSelected(true);
            lockToggle.setText("Unlock all");
        }
    }

    /**
     * Used to fire a new event informing listeners about new child component
     * events.
     *
     * @param tracksEvent The event to handle
     * @param eventObject The event object to repackage
     */
    private void fireTracksControllerEvent(final TracksEvent tracksEvent, final EventObject eventObject) {
        TracksControllerEvent e = new TracksControllerEvent(this, tracksEvent, eventObject);
        Object[] listeners = listenerList.getListenerList();

        synchronized (this) {

            /*
             * The listener list contains the listening class and then the
             * listener instance.
             */
            for (int i = 0; i < listeners.length; i += 2) {

                if (listeners[i] == TracksControllerListener.class) {
                    ((TracksControllerListener) listeners[i + 1]).tracksControllerChanged(e);
                }
            }
        }
    }

    private void handleViewportChanged(final ViewportState oldViewport, final ViewportState newViewport) {
        runInEDT(new Runnable() {
            @Override
            public void run() {
                updateZoomSlide(newViewport);
                updateTracksScrollBar(newViewport);
                tracksScrollPane.repaint();
            }
        });
    }

    @Override
    public void propertyChange(final PropertyChangeEvent evt) {

        if (evt.getSource() == mixerModel.getViewportModel()) {
            final ViewportState oldViewport = (evt.getOldValue() instanceof ViewportState)
                    ? (ViewportState) evt.getOldValue()
                    : null;
            final ViewportState newViewport = (evt.getNewValue() instanceof ViewportState)
                    ? (ViewportState) evt.getNewValue()
                    : null;
            handleViewportChanged(oldViewport, newViewport);
        }
    }

    private void handleResize() {
        final ViewportState viewport = viewportModel.getViewport();

        if (Double.isNaN(viewport.getResolution())) {
            viewportModel.setViewport(viewport.getViewStart(), viewport.getViewEnd(), viewport.getMaxEnd(),
                    timescaleController.getView().getWidth());
        } else {
            viewportModel.resizeViewport(viewportModel.getViewport().getViewStart(),
                    timescaleController.getView().getWidth());
        }
    }

    /** Handles component resizing. */
    private final class SizeHandler extends ComponentAdapter {
        @Override
        public void componentResized(final ComponentEvent e) {
            handleResize();
        }
    }

    private class OSXGestureListener implements MagnificationListener, GesturePhaseListener, SwipeListener {

        /**
         * Cumulative sum of the current zoom gesture, where positive values
         * indicate zooming in (enlarging) and negative values indicate zooming
         * out (shrinking). On a 2009 MacBook Pro, pinch-and-zooming from
         * corner-to-corner of the trackpad will result in a total sum of
         * approximately +3.0 (zooming in) or -3.0 (zooming out).
         */
        private double osxMagnificationGestureSum = 0;

        /** Relative zoom when the magnification gesture began. */
        private double osxMagnificationGestureInitialZoomSetting;

        public void register(final JComponent component) {
            GestureUtilities.addGestureListenerTo(tracksPanel, osxGestureListener);
        }

        /**
         * Invoked when a magnification gesture ("pinch and squeeze") is performed by the user on Mac OS X.
         *
         * @param e contains the scale of the magnification
         */
        @Override
        public void magnify(final MagnificationEvent e) {
            osxMagnificationGestureSum += e.getMagnification();

            /** Amount of the pinch-and-squeeze gesture required to perform a full zoom in the mixer. */
            final double fullZoomMotion = 2.0;
            final double newZoomSetting = Math.min(Math.max(
                    osxMagnificationGestureInitialZoomSetting + (osxMagnificationGestureSum / fullZoomMotion), 0.0),
                    1.0);

            viewportModel.setViewportZoom(newZoomSetting, needleController.getNeedleModel().getCurrentTime());
        }

        /**
         * Indicates that the user has started performing a gesture on Mac OS X.
         */
        @Override
        public void gestureBegan(final GesturePhaseEvent e) {
            osxMagnificationGestureSum = 0;
            osxMagnificationGestureInitialZoomSetting = viewportModel.getViewport().getZoomLevel();
        }

        /**
         * Indicates that the user has finished performing a gesture on Mac OS X.
         */
        @Override
        public void gestureEnded(final GesturePhaseEvent e) {
        }

        @Override
        public void swipedDown(final SwipeEvent e) {
        }

        @Override
        public void swipedLeft(final SwipeEvent e) {
            swipeHorizontal(false);
        }

        @Override
        public void swipedRight(final SwipeEvent e) {
            swipeHorizontal(true);
        }

        private void swipeHorizontal(final boolean swipeLeft) {

            /** The number of horizontal swipe actions needed to move the scroll bar along by the visible amount (i.e. a page left/right action) */
            final int swipesPerVisibleAmount = 5;
            final int newValue = tracksScrollBar.getValue()
                    + ((swipeLeft ? -1 : 1) * tracksScrollBar.getVisibleAmount() / swipesPerVisibleAmount);
            tracksScrollBar.setValue(
                    Math.max(Math.min(newValue, tracksScrollBar.getMaximum() - tracksScrollBar.getVisibleAmount()),
                            tracksScrollBar.getMinimum()));
        }

        @Override
        public void swipedUp(final SwipeEvent e) {
        }
    }
}