/*
 * File:     ElanLayoutManager.java
 * Project:  MPI Linguistic Application
 * Date:     03 April 2006
 *
 * Copyright (C) 2001-2006  Max Planck Institute for Psycholinguistics
 *
 * 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 2 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, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package mpi.eudico.client.annotator;

import mpi.eudico.client.annotator.grid.*;

import mpi.eudico.client.annotator.linkedmedia.MediaDescriptorUtil;

import mpi.eudico.client.annotator.player.*;

import mpi.eudico.client.annotator.viewer.*;

import mpi.eudico.server.corpora.clomimpl.abstr.MediaDescriptor;

import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.GridLayout;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.MouseEvent;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;

import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JMenuBar;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JSplitPane;
import javax.swing.JTabbedPane;
import javax.swing.SwingUtilities;
import javax.swing.border.EmptyBorder;
import javax.swing.event.MouseInputAdapter;


/**
 * Takes care of layout in an ElanFrame
 */
public class ElanLayoutManager implements ElanLocaleListener {
    /* Note: when more modi are added, SyncModeCA should be adapted */

    /** sync layout mode */
    public final static int SYNC_MODE = 0;

    /** normal layout mode */
    public final static int NORMAL_MODE = 1;

    /** the default width for the first video */
    private final static int MASTER_MEDIA_WIDTH = 352;

    /** the minimum height for the first video and the tabpane */
    private final static int MASTER_MEDIA_HEIGHT = 250;

    /** outer content pane margin */
    private final static int CONTAINER_MARGIN = 3;

    /** the default height of the signalviewer */
    private final static int DEF_SIGNAL_HEIGHT = 70;
    private ElanFrame2 elanFrame;
    private Container container;
    private ViewerManager2 viewerManager;
    private int mode;

    //	private boolean normalMode; // can/should? be done with a mode parameter
    //	private boolean syncMode;
    private boolean showTimeLineViewer;
    private boolean showInterlinearViewer;

    // temp permanent detached flag
    private boolean permanentDetached;

    // media players
    private ElanMediaPlayer masterMediaPlayer;

    //private ElanMediaPlayer mediaPlayer2;
    // a list of player layout info objects
    private List playerList;
    private ElanMediaPlayerController mediaPlayerController;
    private SyncManager syncManager;
    private SignalViewer signalViewer;
    private JComponent signalComponent;
    private TimeLineViewer timeLineViewer;
    private InterlinearViewer interlinearViewer;
    private TimeSeriesViewer timeseriesViewer;
    private JPanel timeLineComponent;
    private MultiTierControlPanel multiTierControlPanel;
    private JSplitPane timeLineSplitPane;

    //	private JSplitPane mediaTabSplitPane;
    private JTabbedPane tabPane;
    private JPanel gridPanel;
    private JPanel textPanel;
    private JPanel subtitlePanel;
    private JPanel controlPanel;
    private Component glasspane;

    /**
     * DOCUMENT ME!
     *
     * @param elanFrame the content pane of the ElanFrame
     * @param viewerManager DOCUMENT ME!
     */
    public ElanLayoutManager(ElanFrame2 elanFrame, ViewerManager2 viewerManager) {
        this.elanFrame = elanFrame;
        this.container = elanFrame.getContentPane();
        this.viewerManager = viewerManager;
        playerList = new ArrayList(8);

        // listen to locale changes
        ElanLocale.addElanLocaleListener(viewerManager.getTranscription(), this);

        // no java LayoutManager, we take care of it ourselves
        container.setLayout(null);

        // listen for ComponentEvents on the Container
        container.addComponentListener(new ContainerComponentListener());

        mode = NORMAL_MODE;

        //		normalMode = true;
        //		syncMode = false;
        showTimeLineViewer = true;

        controlPanel = new JPanel();
        controlPanel.setName(ElanLocale.getString("Tab.Controls"));

        //controlPanel.setLayout(null);
        controlPanel.setLayout(new GridLayout(2, 1, 10, 10));
        controlPanel.setBorder(new EmptyBorder(10, 10, 10, 10));

        glasspane = elanFrame.getGlassPane();

        //MouseHandler mh = new MouseHandler();
        //       glasspane.addMouseListener(mh);
        //       glasspane.addMouseMotionListener(mh);
        //       glasspane.setVisible(true);
        // The sync manager is created here because all viewers that are added to
        // this layout manager must also be added to the sync manager for a useable
        // sync layout mode.
        syncManager = new SyncManager(viewerManager, this);

        // initially attached, temp from here
        // temporary code to allow permanent detached mode until swapping is possible on the mac
        Boolean permDetached = (Boolean) Preferences.get("PreferredMediaWindow",
                null);

        if (permDetached == null) {
            permDetached = new Boolean(false); // default usage is attached media window
        }

        permanentDetached = permDetached.booleanValue();

        if (!System.getProperty("os.name").startsWith("Mac OS")) {
            permanentDetached = false;
        }

        // end temp
    }

    /*
     *
     */
    public void updateLocale() {
        if (tabPane != null) {
            int nTabs = tabPane.getTabCount();

            for (int i = 0; i < nTabs; i++) {
                Component component = tabPane.getComponentAt(i);

                if (component == gridPanel) {
                    tabPane.setTitleAt(i, ElanLocale.getString("Tab.Grid"));
                } else if (component == textPanel) {
                    tabPane.setTitleAt(i, ElanLocale.getString("Tab.Text"));
                } else if (component == subtitlePanel) {
                    tabPane.setTitleAt(i, ElanLocale.getString("Tab.Subtitles"));
                } else if (component == controlPanel) {
                    tabPane.setTitleAt(i, ElanLocale.getString("Tab.Controls"));
                }
            }
        }

        if (syncManager != null) {
            syncManager.updateLocale();

            if (mode == SYNC_MODE) {
                doLayout();
            }
        }
    }

    /**
     * Make an object visible in the layout.
     *
     * @param object
     */
    public void add(Object object) {
        if (object instanceof ElanMediaPlayer) {
            addMediaPlayer((ElanMediaPlayer) object);
        } else if (object instanceof ElanMediaPlayerController) {
            setMediaPlayerController((ElanMediaPlayerController) object);
        } else if (object instanceof SignalViewer) {
            setSignalViewer((SignalViewer) object);
        } else if (object instanceof TimeLineViewer) {
            setTimeLineViewer((TimeLineViewer) object);
        } else if (object instanceof InterlinearViewer) {
            setInterlinearViewer((InterlinearViewer) object);
        } else if (object instanceof GridViewer) {
            addSingleTierViewer((SingleTierViewer) object);
        } else if (object instanceof TextViewer) {
            addSingleTierViewer((SingleTierViewer) object);
        } else if (object instanceof SubtitleViewer) {
            addSingleTierViewer((SingleTierViewer) object);
        } else if (object instanceof mpi.eudico.p2p.CollaborationPanel) {
            addToTabPane("P2P", (Component) object);
        } else if (object instanceof TimeSeriesViewer) {
            setTimeSeriesViewer((TimeSeriesViewer) object);
        }
    }

    /**
     * Remove an object from the layout.
     *
     * @param object
     */
    public void remove(Object object) {
        if (object instanceof ElanMediaPlayer) {
            removeMediaPlayer((ElanMediaPlayer) object);
        } else if (object instanceof SignalViewer) {
            removeSignalViewer();
        } else if (object instanceof mpi.eudico.p2p.CollaborationPanel) {
            removeFromTabPane((Component) object);
        }
    }

    /**
     * Make an object invisible in the layout.
     *
     * @param object
     */
    public void hide(Object object) {
    }

    // needed by sync master, not so nice
    public ElanMediaPlayer getMasterMediaPlayer() {
        return masterMediaPlayer;
    }

    /**
     * Add a mediaplayer to the layout- and syncmanager.
     * Before adding/removing a player while in sync mode be sure that all players
     * are connected by calling connectAllPlayers.
     *
     * @see #connectAllPlayers()
     * @param player the player to add
     */
    private void addMediaPlayer(ElanMediaPlayer player) {
        if (player == null) {
            return; //cannot happen now
        }

        if (player instanceof SyncPlayer) {
            player.setLayoutManager(this); //??

            PlayerLayoutModel plModel = new PlayerLayoutModel(player);
            plModel.syncOnly = true;
            playerList.add(plModel);
            syncManager.add(player);
            container.add(syncManager.getPlayerLabel(player));

            if (mode == SYNC_MODE) {
                doLayout();
            }

            return;
        }

        player.setLayoutManager(this);

        PlayerLayoutModel plModel = new PlayerLayoutModel(player);
        playerList.add(plModel);

        // should we check whether the player already has been added before??
        if (permanentDetached) {
            plModel.detach();
        }

        int nextIndex = playerList.size();
        syncManager.add(player);
        container.add(syncManager.getPlayerLabel(player));

        if (plModel.isVisual() && plModel.isAttached()) {
            container.add(plModel.visualComponent);
        }

        if (!containsComponent(syncManager.getPlayerSelectionPanel())) {
            container.add(syncManager.getPlayerSelectionPanel());
        }

        if (nextIndex == 1) {
            masterMediaPlayer = player;
        }

        doLayout(); // maybe call from the outside
    }

    /**
     * Removes a player from the layout and syncmanager.
     * Before adding/removing a player while in sync mode be sure that all players
     * are connected by calling connectAllPlayers.
     *
     * @see #connectAllPlayers()
     * @param player the player to remove
     */
    private void removeMediaPlayer(ElanMediaPlayer player) {
        if (player == null) {
            return;
        }

        PlayerLayoutModel plModel = null;
        int index = -1;

        for (int i = 0; i < playerList.size(); i++) {
            plModel = (PlayerLayoutModel) playerList.get(i);

            if (plModel.player == player) {
                index = i;

                break;
            }
        }

        if (plModel == null) {
            return;
        }

        if (player instanceof SyncPlayer) {
            if (playerList.remove(plModel)) {
                container.remove(syncManager.getPlayerLabel(plModel.player));

                if (plModel.player.getVisualComponent() != null) {
                    container.remove(plModel.player.getVisualComponent());
                }

                syncManager.remove(plModel.player);

                if (mode == SYNC_MODE) {
                    doLayout();
                }
            }

            return;
        }

        container.remove(syncManager.getPlayerLabel(plModel.player));
        syncManager.remove(plModel.player);

        if (plModel.isVisual()) {
            if (plModel.isAttached()) {
                container.remove(plModel.visualComponent);
            } else {
                // attach destroys a detached frame...
                plModel.attach();
            }
        }

        playerList.remove(plModel);

        if ((index == 0) && (playerList.size() > 0)) {
            //the master media has been removed
            PlayerLayoutModel model = (PlayerLayoutModel) playerList.get(0);
            masterMediaPlayer = model.player;
        }

        if (playerList.size() == 0) {
            container.remove(syncManager.getPlayerSelectionPanel());
        }

        doLayout();
    }

    /**
     * Detaches the specified viewer or player.
     *
     * @param object the viewer or player to remove from the main application frame
     */
    public void detach(Object object) {
        if (object instanceof AbstractViewer) {
            container.remove((Component) object);

            // should create a kind of list of ViewerLayoutModel objects 
            // once attaching and detaching of viewers is implemented
            String title = object.getClass().getName();
            int index = title.lastIndexOf('.');

            if (index > 0) {
                title = title.substring(index + 1, title.length());
            }

            JDialog dlg = new DetachedViewerFrame(this, (Component) object,
                    title);
            dlg.setSize(500, 300);
            dlg.setVisible(true);
        } else if (object instanceof Component) {
            // might be a visual component of a player
            PlayerLayoutModel model;

            for (int i = 0; i < playerList.size(); i++) {
                model = (PlayerLayoutModel) playerList.get(i);

                if (model.visualComponent == object) {
                    if (model.isVisual() && model.isAttached()) {
                        container.remove(model.visualComponent);
                        model.detach();

                        doLayout();
                    }

                    break;
                }
            }
        }
    }

    /**
     * Attaches the specified viewer or player.
     *
     * @param object the viewer or player to attach
     */
    public void attach(Object object) {
        /*
        if (object instanceof AbstractViewer) {
            // detach from frame/dialog, destroy dialog and add to container
            // use a ViewerLayoutModel
            container.add((Component)object);
        } else */
        if (object instanceof Component) {
            // might be a visual component of a player
            PlayerLayoutModel model;

            for (int i = 0; i < playerList.size(); i++) {
                model = (PlayerLayoutModel) playerList.get(i);

                if (model.visualComponent == object) {
                    if (model.isVisual() && !model.isAttached()) {
                        model.attach();
                        container.add(model.visualComponent);

                        doLayout();

                        // test for QT
                        if (model.player instanceof QTMediaPlayer) {
                            // force movie to resize					
                            try {
                                Thread.sleep(100);
                            } catch (Exception e) {
                            }

                            Rectangle rec = model.visualComponent.getBounds();
                            model.visualComponent.setBounds(new Rectangle(
                                    rec.x, rec.y, rec.width - 1, rec.height -
                                    1));
                            container.validate();
                        }
                    }

                    break;
                }
            }
        }
    }

    /**
     * When more than one video players are present and attached, this will make
     * the specified video the one that is displayed as the first (and largest)
     * video.
     * @param player the player to display as the first (attached) video
     */
    public void setFirstPlayer(ElanMediaPlayer player) {
        if (player == null) {
            return;
        }

        PlayerLayoutModel model;

        for (int i = 0; i < playerList.size(); i++) {
            model = (PlayerLayoutModel) playerList.get(i);

            if (model.player == player) {
                model.setDisplayedFirst(true);
            } else {
                model.setDisplayedFirst(false);
            }
        }

        doLayout();
    }

    /**
     * DOCUMENT ME!
     *
     * @param mediaPlayerController
     */
    private void setMediaPlayerController(
        ElanMediaPlayerController mediaPlayerController) {
        this.mediaPlayerController = mediaPlayerController;

        // add the control components to the container
        container.add(mediaPlayerController.getPlayButtonsPanel());
        container.add(mediaPlayerController.getTimePanel());

        //		container.add(mediaPlayerController.getDurationPanel());
        container.add(mediaPlayerController.getModePanel());
        container.add(mediaPlayerController.getSelectionPanel());
        container.add(mediaPlayerController.getSelectionButtonsPanel());
        container.add(mediaPlayerController.getAnnotationNavigationPanel());
        container.add(mediaPlayerController.getSliderPanel());
        container.add(mediaPlayerController.getAnnotationDensityViewer());

        controlPanel.add(mediaPlayerController.getVolumePanel());
        controlPanel.add(mediaPlayerController.getRatePanel());

        addToTabPane(ElanLocale.getString("Tab.Controls"), controlPanel);
        doLayout();
    }

    /**
     * DOCUMENT ME!
     *
     * @param signalViewer
     */
    private void setSignalViewer(SignalViewer signalViewer) {
        this.signalViewer = signalViewer;

        if (signalComponent == null) { // dit al voorbakken
            signalComponent = new JPanel();
            signalComponent.setLayout(null);
            signalComponent.addComponentListener(new SignalSplitPaneListener());
        }

        // place the component in the split pane
        signalComponent.add(signalViewer);
        getTimeLineSplitPane().setTopComponent(signalComponent);

        int divLoc = (signalComponent.getHeight() < DEF_SIGNAL_HEIGHT)
            ? DEF_SIGNAL_HEIGHT : signalComponent.getHeight();
        timeLineSplitPane.setDividerLocation(divLoc);

        doLayout();
    }

    /**
     * Returns the SignalViewer.
     *
     * @return the SignalViewer, can be null
     */
    public SignalViewer getSignalViewer() {
        return signalViewer;
    }

    /**
     * Removes the SignalViewer from the layout.
     */
    private void removeSignalViewer() {
        if (signalViewer != null) {
            if (signalComponent != null) {
                signalComponent.remove(signalViewer);
                getTimeLineSplitPane().setTopComponent(null);

                //timeLineSplitPane.setDividerLocation(0);
                doLayout();
            }
        }
    }

    /**
     * DOCUMENT ME!
     *
     * @param timeLineViewer
     */
    private void setTimeLineViewer(TimeLineViewer timeLineViewer) {
        this.timeLineViewer = timeLineViewer;

        if (timeLineComponent == null) {
            timeLineComponent = new JPanel();
            timeLineComponent.setLayout(null);
        }

        if (multiTierControlPanel == null) {
            multiTierControlPanel = viewerManager.getMultiTierControlPanel();
            timeLineComponent.add(multiTierControlPanel);
        }

        // disable the interlinear viewer if it exists
        if (interlinearViewer != null) {
            viewerManager.disableViewer(interlinearViewer);
        }

        // place the component in the split pane
        timeLineComponent.add(timeLineViewer);
        getTimeLineSplitPane().setBottomComponent(timeLineComponent);

        doLayout();
    }

    /**
     * DOCUMENT ME!
     *
     * @param interlinearViewer
     */
    private void setInterlinearViewer(InterlinearViewer interlinearViewer) {
        this.interlinearViewer = interlinearViewer;

        if (timeLineComponent == null) {
            timeLineComponent = new JPanel();
            timeLineComponent.setLayout(null);
        }

        if (multiTierControlPanel == null) {
            multiTierControlPanel = viewerManager.getMultiTierControlPanel();
            timeLineComponent.add(multiTierControlPanel);
        }

        // disable the interlinear viewer if it exists
        if (timeLineViewer != null) {
            viewerManager.disableViewer(timeLineViewer);
        }

        // place the component in the split pane
        timeLineComponent.add(interlinearViewer);
        getTimeLineSplitPane().setBottomComponent(timeLineComponent);

        doLayout();
    }

    /**
     * Sets the one timeseries viewer. This has to be changed if there is a need
     * for more than one timeseries viewer.
     *
     * @param viewer the timeseries viewer
     */
    private void setTimeSeriesViewer(TimeSeriesViewer timeseriesViewer) {
        this.timeseriesViewer = timeseriesViewer;

        // add to the layout if there is no SignalViewer??
        // for now just create a detached frame for the viewer
        detach(timeseriesViewer);
    }

    /**
     * DOCUMENT ME!
     *
     * @return DOCUMENT ME!
     */
    public MultiTierControlPanel getMultiTierControlPanel() {
        return multiTierControlPanel;
    }

    /**
     * DOCUMENT ME!
     */
    public void showTimeLineViewer() {
        showTimeLineViewer = true;
        showInterlinearViewer = false;

        enableDisableLogic();

        doLayout();
    }

    /**
     * DOCUMENT ME!
     */
    public void showInterlinearViewer() {
        showTimeLineViewer = false;
        showInterlinearViewer = true;

        enableDisableLogic();

        doLayout();
    }

    private void enableDisableLogic() {
        if (showTimeLineViewer) {
            if (timeLineViewer != null) {
                viewerManager.enableViewer(timeLineViewer);
            }

            if (interlinearViewer != null) {
                viewerManager.disableViewer(interlinearViewer);
            }
        } else if (showInterlinearViewer) {
            if (timeLineViewer != null) {
                viewerManager.disableViewer(timeLineViewer);
            }

            if (interlinearViewer != null) {
                viewerManager.enableViewer(interlinearViewer);
            }
        }
    }

    private JSplitPane getTimeLineSplitPane() {
        if (timeLineSplitPane == null) {
            timeLineSplitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
            timeLineSplitPane.setOneTouchExpandable(true);

            // HS 24 nov set the divider location when a top component is added
            timeLineSplitPane.setDividerLocation(0);
            timeLineSplitPane.setContinuousLayout(true);
            container.add(timeLineSplitPane);
        }

        return timeLineSplitPane;
    }

    private void addSingleTierViewer(SingleTierViewer viewer) {
        SingleTierViewerPanel panel = viewerManager.createSingleTierViewerPanel();
        panel.setViewer(viewer);

        if (viewer instanceof GridViewer) {
            gridPanel = panel;
            addToTabPane(ElanLocale.getString("Tab.Grid"), panel);
        } else if (viewer instanceof TextViewer) {
            textPanel = panel;
            addToTabPane(ElanLocale.getString("Tab.Text"), panel);
        } else if (viewer instanceof SubtitleViewer) {
            getSubtitlePanel().add(panel);
        }

        doLayout();
    }

    private JPanel getSubtitlePanel() {
        if (subtitlePanel == null) {
            subtitlePanel = new JPanel(new GridLayout(0, 1));
            addToTabPane(ElanLocale.getString("Tab.Subtitles"), subtitlePanel);
        }

        return subtitlePanel;
    }

    private JTabbedPane getTabPane() {
        if (tabPane == null) {
            tabPane = new JTabbedPane();
            container.add(tabPane);
        }

        return tabPane;
    }

    private void addToTabPane(String tabName, Component component) {
        int index = getTabPane().getTabCount();

        if ((controlPanel != null) && (component != controlPanel)) {
            index = getTabPane().indexOfComponent(controlPanel);
        }

        getTabPane().insertTab(tabName, null, component, null, index);

        /*
        if (tabName != null) {
        //getTabPane().add(tabName, component);
        getTabPane().insertTab(tabName, null, component, null, index);
        } else {
        //getTabPane().add(component);
        getTabPane().insertTab(tabName, null, component, null, index);
        }
        */
        /*
        // always keep controls as last tab
        if (controlPanel != null) {
            getTabPane().remove(controlPanel);
            getTabPane().add(controlPanel, getTabPane().getTabCount());
        }
        */
        doLayout();
    }

    private void removeFromTabPane(Component component) {
        getTabPane().remove(component);
    }

    /**
     * Switches to annotation layout mode.
     */
    public void showNormalMode() {
        // if old mode == sync, then check offsets of video and extracted audio 
        // and warn if they are not equal and the video is not the master
        if (mode == SYNC_MODE) {
            checkUnequalOffsets();

            for (int i = 0; i < playerList.size(); i++) {
                PlayerLayoutModel plm = (PlayerLayoutModel) playerList.get(i);

                if (plm.syncOnly) {
                    viewerManager.destroyMediaPlayer(plm.player);

                    if (plm.player.getVisualComponent() != null) {
                        container.remove(plm.player.getVisualComponent());
                    }
                }
            }
        }

        mode = NORMAL_MODE;

        //		normalMode = true;
        //		syncMode = false;
        // make sure the last offset is also frozen. The call is done here
        // because at this moment SyncManager is only known here
        syncManager.reconnect();

        // make sure the signal viewer has the right offset, again this is done
        // here because all the information is available here at the right moment
        if (signalViewer != null) {
            signalViewer.setOffset(viewerManager.getSignalViewerOffset());
        }

        // time panel can be removed by SyncManager
        if (!containsComponent(mediaPlayerController.getTimePanel())) {
            container.add(mediaPlayerController.getTimePanel());
        }

        if (masterMediaPlayer != null) {
            viewerManager.setMasterMediaPlayer(masterMediaPlayer);
        }

        viewerManager.enableDisabledMediaPlayers();

        // all viewers should be disconnected but for the time being only the problematic ones
        if (timeLineViewer != null) {
            viewerManager.enableViewer(timeLineViewer);
        }

        if (signalViewer != null) {
            viewerManager.enableViewer(signalViewer);
        }

        clearLayout();
        doLayout();

        // this sucks but is needed to make Swing behave properly

        /*
        Dimension dimension = container.getSize();
        container.setSize(dimension.width - 1, dimension.height - 1);
        container.setSize(dimension);
        */
    }

    /**
     * Switches to media synchronization mode.
     */
    public void showSyncMode() {
        mode = SYNC_MODE;

        // add visual component of sync_only_players
        for (int i = 0; i < playerList.size(); i++) {
            PlayerLayoutModel plm = (PlayerLayoutModel) playerList.get(i);

            if (plm.syncOnly) {
                viewerManager.addMediaPlayer(plm.player);

                if (plm.player.getVisualComponent() != null) {
                    container.add(plm.player.getVisualComponent());
                }
            }
        }

        viewerManager.setMasterMediaPlayer(masterMediaPlayer);
        viewerManager.enableDisabledMediaPlayers();

        // all currently active viewers should be disconnected
        if (timeLineViewer != null) {
            viewerManager.disableViewer(timeLineViewer);
        }

        if (signalViewer != null) {
            viewerManager.disableViewer(signalViewer);
        }

        clearLayout();
        doLayout();

        // this sucks but is needed to make Swing behave properly

        /*
        Dimension dimension = container.getSize();
        container.setSize(dimension.width - 1, dimension.height - 1);
        container.setSize(dimension);
        */
    }

    /**
     * Returns the current layout mode.
     *
     * @return the current layout mode
     */
    public int getMode() {
        return mode;
    }

    /**
     * Makes sure all players are connected. Current offsets in the
     * sync mode are ignored accept for the current active player.
     */
    public void connectAllPlayers() {
        if (mode == SYNC_MODE) {
            syncManager.reconnect();
        }
    }

    /**
     * Make all components invisible
     */
    private void clearLayout() {
        Component[] components = container.getComponents();

        for (int i = 0; i < components.length; i++) {
            components[i].setBounds(0, 0, 0, 0);
        }
    }

    /**
     * Checks the layout Mode flag and calls either doNormalLayout() or
     * doSyncLayout().
     */
    public void doLayout() {
        if (mode == NORMAL_MODE) {
            doNormalLayout();
        } else if (mode == SYNC_MODE) {
            doSyncLayout();
        }
    }

    /**
     * Show the layout for synchronizing two or more video and audio players.
     */
    private void doSyncLayout() {
        // get the width and height of the usable area
        int containerWidth = container.getWidth();
        int containerHeight = container.getHeight();
        int containerMargin = 3;
        int componentMargin = 5;
        int controlsMargin = 20;
        int labelHeight = 20;

        if (playerList.size() > 0) {
            for (int i = 0; i < playerList.size(); i++) {
                PlayerLayoutModel m = (PlayerLayoutModel) playerList.get(i);
                Dimension prefSize = syncManager.getPlayerLabel(m.player)
                                                .getPreferredSize();

                if (prefSize.height > labelHeight) {
                    labelHeight = prefSize.height;
                }
            }
        }

        // initialize some fields
        int playerSelectionWidth = 130;
        int playerSelectionHeight = 70;
        Dimension playerSelectionSize = syncManager.getPlayerSelectionPanel()
                                                   .getPreferredSize();
        playerSelectionWidth = Math.max(playerSelectionWidth,
                playerSelectionSize.width);
        playerSelectionHeight = Math.max(playerSelectionHeight,
                playerSelectionSize.height);

        int controlsHeight = (2 * componentMargin) +
            mediaPlayerController.getSliderPanel().getPreferredSize().height +
            playerSelectionHeight;
        int totalPlayerWidth = containerWidth - (2 * containerMargin);
        int totalPlayerHeight = containerHeight - containerMargin -
            controlsHeight;

        int[] colrow = getNumberOfColumnsAndRows(totalPlayerWidth,
                totalPlayerHeight, labelHeight, playerList.size());

        // conditionally layout players	
        int mediaY = containerMargin;
        int mediaX = containerMargin;

        if ((colrow[0] > 0) && (colrow[1] > 0)) {
            int maxMediaWidth = (totalPlayerWidth / colrow[0]) -
                componentMargin;
            int maxMediaHeight = (totalPlayerHeight / colrow[1]) - labelHeight -
                componentMargin;

            if ((maxMediaWidth > 0) && (maxMediaHeight > 0)) {
                int playerIndex = 0;

                // layout per row
                for (int i = 0; i < colrow[1]; i++) {
                    mediaY += (i * (maxMediaHeight + labelHeight));

                    for (int j = 0; j < colrow[0]; j++) {
                        PlayerLayoutModel model = null;
                        ElanMediaPlayer player = null;

                        if (playerIndex < playerList.size()) {
                            model = (PlayerLayoutModel) playerList.get(playerIndex);
                            player = model.player;
                        }

                        Component label = syncManager.getPlayerLabel(player);
                        int curX = mediaX +
                            (j * (maxMediaWidth + componentMargin));
                        int curY = mediaY;
                        int curW = maxMediaWidth;
                        int curH = maxMediaHeight;

                        if (label != null) {
                            Dimension prefLabelSize = label.getPreferredSize();

                            if (prefLabelSize.width <= maxMediaWidth) {
                                label.setBounds(curX, curY + curH, curW,
                                    labelHeight);
                            } else {
                                label.setBounds(0, 0, 0, 0);
                            }
                        }

                        Component videoComp = null;
                        float aspectRatio = 1.0f;

                        if (model.syncOnly) {
                            videoComp = model.player.getVisualComponent();
                            aspectRatio = model.player.getAspectRatio();
                        } else if (model.isVisual() && model.isAttached()) {
                            videoComp = model.visualComponent;
                            aspectRatio = model.player.getAspectRatio();
                        }

                        if (videoComp != null) {
                            //Component videoComp = model.visualComponent;
                            //float aspectRatio = model.player.getAspectRatio();
                            curH = (int) ((float) curW / aspectRatio);

                            if (curH > maxMediaHeight) {
                                curH = maxMediaHeight;
                                curW = (int) (curH * aspectRatio);
                                curX += ((maxMediaWidth - curW) / 2);
                            }

                            videoComp.setBounds(curX, curY, curW, curH);
                        }

                        playerIndex++;

                        if (playerIndex >= playerList.size()) {
                            break;
                        }
                    }

                    if (playerIndex >= playerList.size()) {
                        mediaY += (maxMediaHeight + labelHeight +
                        componentMargin);

                        break;
                    }
                }
            }
        }

        int controlsY = mediaY;

        int sliderPanelX = CONTAINER_MARGIN + controlsMargin;
        int sliderPanelY = controlsY;
        int sliderPanelWidth = 0;
        int sliderPanelHeight = 0;

        if (mediaPlayerController != null) {
            sliderPanelWidth = containerWidth - (2 * CONTAINER_MARGIN) -
                (2 * controlsMargin);
            sliderPanelHeight = mediaPlayerController.getSliderPanel()
                                                     .getPreferredSize().height;
            mediaPlayerController.getSliderPanel().setBounds(sliderPanelX,
                sliderPanelY, sliderPanelWidth, sliderPanelHeight);
        }

        int timePanelX = 0;
        int timePanelY = sliderPanelY + sliderPanelHeight + componentMargin;
        int timePanelWidth = 0;
        int timePanelHeight = 0;

        if ((mediaPlayerController != null) && syncManager.connectedPlayers()) {
            // time panel can be removed by SyncManager
            if (!containsComponent(mediaPlayerController.getTimePanel())) {
                container.add(mediaPlayerController.getTimePanel());
            }

            timePanelX = (containerWidth / 2) -
                (mediaPlayerController.getTimePanel().getPreferredSize().width / 2);
            timePanelWidth = mediaPlayerController.getTimePanel()
                                                  .getPreferredSize().width;
            timePanelHeight = mediaPlayerController.getTimePanel()
                                                   .getPreferredSize().height;
            mediaPlayerController.getTimePanel().setBounds(timePanelX,
                timePanelY, timePanelWidth, timePanelHeight);
        }

        int playButtonsX = 0;
        int playButtonsY = timePanelY + 40;
        int playButtonsWidth = 0;
        int playButtonsHeight = 0;

        if (mediaPlayerController != null) {
            playButtonsX = (containerWidth / 2) -
                (mediaPlayerController.getPlayButtonsPanel().getPreferredSize().width / 2);
            playButtonsWidth = mediaPlayerController.getPlayButtonsPanel()
                                                    .getPreferredSize().width;
            playButtonsHeight = mediaPlayerController.getPlayButtonsPanel()
                                                     .getPreferredSize().height;

            // adjust the position to prevent overlap with the player selection panel
            if (playButtonsX < (sliderPanelX + playerSelectionWidth +
                    componentMargin)) {
                playButtonsX = sliderPanelX + playerSelectionWidth +
                    componentMargin;

                int adjTimePanelX = playButtonsX +
                    ((playButtonsWidth - timePanelWidth) / 2);
                mediaPlayerController.getTimePanel().setBounds(adjTimePanelX,
                    timePanelY, timePanelWidth, timePanelHeight);
            }

            mediaPlayerController.getPlayButtonsPanel().setBounds(playButtonsX,
                playButtonsY, playButtonsWidth, playButtonsHeight);
        }

        int playerSelectionX = playButtonsX - playerSelectionWidth -
            componentMargin;

        if (playerSelectionX < sliderPanelX) {
            playerSelectionX = sliderPanelX;
        }

        int playerSelectionY = timePanelY;

        syncManager.getPlayerSelectionPanel().setBounds(playerSelectionX,
            playerSelectionY, playerSelectionWidth, playerSelectionHeight);

        container.validate();
        container.repaint();
    }

    /**
     * Show the layout for media players and elan viewers
     */
    private void doNormalLayout() {
        // get the width and height of the usable area
        int containerWidth = container.getWidth();
        int containerHeight = container.getHeight();
        int containerMargin = 3;
        int componentMargin = 5;

        PlayerLayoutModel[] visualPlayers = getAttachedVisualPlayers();
        int numVisualPlayers = visualPlayers.length;

        // first layout the player components, next the tabpane
        int visibleMediaX = containerMargin;
        int visibleMediaY = containerMargin;
        int visibleMediaWidth = 0;
        int visibleMediaHeight = 0;

        int firstMediaWidth = visibleMediaWidth;

        if (numVisualPlayers == 0) {
            visibleMediaHeight = MASTER_MEDIA_HEIGHT;
        }

        if (numVisualPlayers >= 1) {
            // layout the first video
            Component firstVisualComp = visualPlayers[0].visualComponent;
            float aspectRatio = visualPlayers[0].player.getAspectRatio();
            firstMediaWidth = MASTER_MEDIA_WIDTH;

            int firstMediaHeight = (int) ((float) firstMediaWidth / aspectRatio);

            if (firstMediaHeight < MASTER_MEDIA_HEIGHT) {
                firstMediaHeight = MASTER_MEDIA_HEIGHT;
                firstMediaWidth = (int) (firstMediaHeight * aspectRatio);
            }

            firstVisualComp.setBounds(visibleMediaX, visibleMediaY,
                firstMediaWidth, firstMediaHeight);
            visibleMediaWidth = firstMediaWidth + componentMargin;
            visibleMediaHeight = firstMediaHeight;
        }

        if (numVisualPlayers == 2) {
            Component secondVisualComp = visualPlayers[1].visualComponent;
            float aspectRatio = visualPlayers[1].player.getAspectRatio();
            int secondMediaWidth = (int) (visibleMediaHeight * aspectRatio);
            secondVisualComp.setBounds(visibleMediaX + visibleMediaWidth,
                visibleMediaY, secondMediaWidth, visibleMediaHeight);
            visibleMediaWidth += (secondMediaWidth + componentMargin);
        } else if (numVisualPlayers == 3) {
            Component secondVisualComp = visualPlayers[1].visualComponent;
            float secondAR = visualPlayers[1].player.getAspectRatio();
            Component thirdVisualComp = visualPlayers[2].visualComponent;
            float thirdAR = visualPlayers[2].player.getAspectRatio();
            int heightPerPlayer = (visibleMediaHeight - componentMargin) / 2;
            int secondWidth = (int) (secondAR * heightPerPlayer);
            int thirdWidth = (int) (thirdAR * heightPerPlayer);
            int widthPerPlayer = Math.max(secondWidth, thirdWidth);
            secondVisualComp.setBounds(visibleMediaX + visibleMediaWidth +
                ((widthPerPlayer - secondWidth) / 2), visibleMediaY,
                secondWidth, heightPerPlayer);
            thirdVisualComp.setBounds(visibleMediaX + visibleMediaWidth +
                ((widthPerPlayer - thirdWidth) / 2),
                visibleMediaY + heightPerPlayer + componentMargin, thirdWidth,
                heightPerPlayer);
            visibleMediaWidth += (widthPerPlayer + componentMargin);
        } else if (numVisualPlayers == 4) {
            Component secondVisualComp = visualPlayers[1].visualComponent;
            float secondAR = visualPlayers[1].player.getAspectRatio();
            Component thirdVisualComp = visualPlayers[2].visualComponent;
            float thirdAR = visualPlayers[2].player.getAspectRatio();
            Component fourthVisualComp = visualPlayers[3].visualComponent;
            float fourthAR = visualPlayers[3].player.getAspectRatio();
            int heightPerPlayer = (visibleMediaHeight - (2 * componentMargin)) / 3;
            int secondWidth = (int) (secondAR * heightPerPlayer);
            int thirdWidth = (int) (thirdAR * heightPerPlayer);
            int fourthWidth = (int) (fourthAR * heightPerPlayer);
            int widthPerPlayer = Math.max(secondWidth, thirdWidth);
            widthPerPlayer = Math.max(widthPerPlayer, fourthWidth);
            secondVisualComp.setBounds(visibleMediaX + visibleMediaWidth +
                ((widthPerPlayer - secondWidth) / 2), visibleMediaY,
                secondWidth, heightPerPlayer);
            thirdVisualComp.setBounds(visibleMediaX + visibleMediaWidth +
                ((widthPerPlayer - thirdWidth) / 2),
                visibleMediaY + heightPerPlayer + componentMargin, thirdWidth,
                heightPerPlayer);
            fourthVisualComp.setBounds(visibleMediaX + visibleMediaWidth +
                ((widthPerPlayer - fourthWidth) / 2),
                visibleMediaY + (2 * heightPerPlayer) + (2 * componentMargin),
                fourthWidth, heightPerPlayer);
            visibleMediaWidth += (widthPerPlayer + componentMargin);
        }

        // layout the tab panel
        int tabPaneX = visibleMediaX + visibleMediaWidth;
        int tabPaneY = visibleMediaY;
        int tabPaneWidth = containerWidth - tabPaneX;
        int tabPaneHeight = visibleMediaHeight;

        if (tabPane != null) {
            tabPane.setBounds(tabPaneX, tabPaneY, tabPaneWidth, tabPaneHeight);

            if ((mediaPlayerController != null) && (controlPanel != null)) {
                controlPanel.setSize(tabPaneWidth, tabPaneHeight);
            }
        }

        int timePanelX = 0;
        int timePanelY = visibleMediaY + visibleMediaHeight + 2;
        int timePanelWidth = 0;
        int timePanelHeight = 0;

        if (mediaPlayerController != null) {
            timePanelWidth = mediaPlayerController.getTimePanel()
                                                  .getPreferredSize().width;
            timePanelHeight = mediaPlayerController.getTimePanel()
                                                   .getPreferredSize().height;

            if (numVisualPlayers == 0) {
                timePanelX = containerMargin;
            } else {
                timePanelX = (containerMargin + (firstMediaWidth / 2)) -
                    (timePanelWidth / 2);
            }

            mediaPlayerController.getTimePanel().setBounds(timePanelX,
                timePanelY, timePanelWidth, timePanelHeight);
        }

        int playButtonsX = CONTAINER_MARGIN;
        int playButtonsY = timePanelY + timePanelHeight + 4;
        int playButtonsWidth = 0;
        int playButtonsHeight = 0;

        if (mediaPlayerController != null) {
            playButtonsWidth = mediaPlayerController.getPlayButtonsPanel()
                                                    .getPreferredSize().width;
            playButtonsHeight = mediaPlayerController.getPlayButtonsPanel()
                                                     .getPreferredSize().height;

            if (numVisualPlayers > 0) {
                playButtonsX = (containerMargin + (firstMediaWidth / 2)) -
                    (playButtonsWidth / 2);

                if (playButtonsX < CONTAINER_MARGIN) {
                    playButtonsX = CONTAINER_MARGIN;
                }
            }

            mediaPlayerController.getPlayButtonsPanel().setBounds(playButtonsX,
                playButtonsY, playButtonsWidth, playButtonsHeight);
        }

        int selectionPanelX = playButtonsX + playButtonsWidth + 20;
        int selectionPanelY = visibleMediaY + visibleMediaHeight + 2;
        int selectionPanelWidth = 0;
        int selectionPanelHeight = 0;

        if (mediaPlayerController != null) {
            selectionPanelWidth = 100 +
                mediaPlayerController.getSelectionPanel().getPreferredSize().width;
            selectionPanelHeight = mediaPlayerController.getSelectionPanel()
                                                        .getPreferredSize().height;
            mediaPlayerController.getSelectionPanel().setBounds(selectionPanelX,
                selectionPanelY, selectionPanelWidth, selectionPanelHeight);
        }

        int selectionButtonsX = selectionPanelX;
        int selectionButtonsY = selectionPanelY + selectionPanelHeight + 4;
        int selectionButtonsWidth = 0;
        int selectionButtonsHeight = 0;

        if (mediaPlayerController != null) {
            selectionButtonsWidth = mediaPlayerController.getSelectionButtonsPanel()
                                                         .getPreferredSize().width;
            selectionButtonsHeight = mediaPlayerController.getSelectionButtonsPanel()
                                                          .getPreferredSize().height;
            mediaPlayerController.getSelectionButtonsPanel().setBounds(selectionButtonsX,
                selectionButtonsY, selectionButtonsWidth, selectionButtonsHeight);
        }

        int annotationButtonsX = selectionButtonsX + selectionButtonsWidth +
            15;
        int annotationButtonsY = selectionPanelY + selectionPanelHeight + 4;
        int annotationButtonsWidth = 0;
        int annotationButtonsHeight = 0;

        if (mediaPlayerController != null) {
            annotationButtonsWidth = mediaPlayerController.getAnnotationNavigationPanel()
                                                          .getPreferredSize().width;
            annotationButtonsHeight = mediaPlayerController.getAnnotationNavigationPanel()
                                                           .getPreferredSize().height;
            mediaPlayerController.getAnnotationNavigationPanel().setBounds(annotationButtonsX,
                annotationButtonsY, annotationButtonsWidth,
                annotationButtonsHeight);
        }

        int modePanelX = annotationButtonsX + annotationButtonsWidth + 10;
        int modePanelY = annotationButtonsY - 1;
        int modePanelWidth = 0;
        int modePanelHeight = 0;

        if (mediaPlayerController != null) {
            modePanelWidth = 300; //mediaPlayerController.getModePanel().getPreferredSize().width;
            modePanelHeight = mediaPlayerController.getModePanel()
                                                   .getPreferredSize().height;
            mediaPlayerController.getModePanel().setBounds(modePanelX,
                modePanelY, modePanelWidth, modePanelHeight);
        }

        int sliderPanelX = CONTAINER_MARGIN;
        int sliderPanelY = playButtonsY + playButtonsHeight;
        int sliderPanelWidth = 0;
        int sliderPanelHeight = 0;

        if (mediaPlayerController != null) {
            sliderPanelWidth = containerWidth - (2 * CONTAINER_MARGIN);
            sliderPanelHeight = mediaPlayerController.getSliderPanel()
                                                     .getPreferredSize().height;
            mediaPlayerController.getSliderPanel().setBounds(sliderPanelX,
                sliderPanelY, sliderPanelWidth, sliderPanelHeight);
        }

        int densityPanelX = CONTAINER_MARGIN;
        int densityPanelY = sliderPanelY + componentMargin; //sliderPanelHeight;
        int densityPanelWidth = sliderPanelWidth;
        int densityPanelHeight = 0;

        if (mediaPlayerController != null) {
            densityPanelHeight = mediaPlayerController.getAnnotationDensityViewer()
                                                      .getPreferredSize().height;
            mediaPlayerController.getAnnotationDensityViewer().setBounds(densityPanelX,
                densityPanelY, densityPanelWidth, densityPanelHeight);
        }

        // layout time line split pane
        int timeLineSplitPaneX = CONTAINER_MARGIN;
        int timeLineSplitPaneY = densityPanelY + densityPanelHeight + 4;
        int timeLineSplitPaneWidth = 0;
        int timeLineSplitPaneHeight = 0;

        if (timeLineSplitPane != null) {
            timeLineSplitPaneWidth = containerWidth - (2 * CONTAINER_MARGIN);
            timeLineSplitPaneHeight = containerHeight - timeLineSplitPaneY;
            timeLineSplitPane.setBounds(timeLineSplitPaneX, timeLineSplitPaneY,
                timeLineSplitPaneWidth, timeLineSplitPaneHeight);
        }

        // layout time line pane
        int multiTierControlX = 0;
        int multiTierControlY = 0;
        int multiTierControlWidth = 0;
        int multiTierControlHeight = 0;
        int timeLineX = 0;
        int timeLineY = 0;
        int timeLineWidth = 0;
        int timeLineHeight = 0;
        int interlinearX = 0;
        int interlinearY = 0;
        int interlinearWidth = 0;
        int interlinearHeight = 0;

        if (timeLineComponent != null) {
            int bottomHeight = timeLineSplitPane.getHeight() -
                timeLineSplitPane.getDividerLocation() -
                timeLineSplitPane.getDividerSize();
            multiTierControlWidth = multiTierControlPanel.getWidth();
            multiTierControlHeight = bottomHeight; //timeLineComponent.getHeight();
            multiTierControlPanel.setBounds(multiTierControlX,
                multiTierControlY, multiTierControlWidth, multiTierControlHeight);

            if (showTimeLineViewer) {
                timeLineX = multiTierControlWidth;

                //timeLineWidth = timeLineComponent.getWidth() - multiTierControlWidth;
                timeLineWidth = timeLineSplitPane.getWidth() -
                    multiTierControlWidth;
                timeLineHeight = bottomHeight; //timeLineComponent.getHeight();
            } else {
                interlinearX = multiTierControlWidth;

                //interlinearWidth = timeLineComponent.getWidth() - multiTierControlWidth;
                interlinearWidth = timeLineSplitPane.getWidth() -
                    multiTierControlWidth;
                interlinearHeight = bottomHeight; //timeLineComponent.getHeight();
            }

            if (timeLineViewer != null) {
                timeLineViewer.setBounds(timeLineX, timeLineY, timeLineWidth,
                    timeLineHeight);
                timeLineViewer.setPreferredSize(new Dimension(timeLineWidth,
                        timeLineHeight));

                // force a component event on the viewer, does not happen automatically apparently
                timeLineViewer.componentResized(null);
            }

            if (interlinearViewer != null) {
                interlinearViewer.setBounds(interlinearX, interlinearY,
                    interlinearWidth, interlinearHeight);
                interlinearViewer.setPreferredSize(new Dimension(
                        interlinearWidth, interlinearHeight));

                // force a component event on the viewer, does not happen automatically apparently
                interlinearViewer.componentResized(null);
            }
        }

        // layout signal pane
        int signalX = multiTierControlWidth;
        int signalY = 0;
        int signalWidth = 0;
        int signalHeight = 0;

        if ((signalComponent != null) && (signalViewer != null)) {
            int rMargin = 0;

            if (timeLineViewer != null) {
                rMargin = timeLineViewer.getRightMargin();
            }

            //		 signalWidth = signalComponent.getWidth() - multiTierControlWidth - rMargin;
            signalWidth = timeLineSplitPane.getWidth() - multiTierControlWidth -
                rMargin;
            signalHeight = signalComponent.getHeight();

            signalViewer.setBounds(signalX, signalY, signalWidth, signalHeight);
            signalComponent.setPreferredSize(new Dimension(signalWidth,
                    signalHeight));
        }

        if (timeLineSplitPane != null) {
            timeLineSplitPane.resetToPreferredSizes();
        }

        // layout control panel components
        /*
        if (mediaPlayerController != null) {

            int vX = 10;
            int vY = 30;
            int vWidth = controlPanel.getWidth() - (2 * vX);
            int vHeight = mediaPlayerController.getVolumePanel()
                                               .getPreferredSize().height;
            mediaPlayerController.getVolumePanel().setBounds(vX, vY, vWidth,
                vHeight);


            int rX = 10;
            int rY = vY + vHeight + 10;
            int rWidth = controlPanel.getWidth() - (2 * vX); //mediaPlayerController.getRatePanel().getPreferredSize().width;
            int rHeight = mediaPlayerController.getRatePanel().getPreferredSize().height;
            mediaPlayerController.getRatePanel().setBounds(rX, rY, rWidth,
                rHeight);

        }
        */
        container.validate();
    }

    /**
     * Helper class to detect if a Component is added to the content pane of
     * the Elan frame.
     *
     * @param component the component that is looked for in the content pane
     *
     * @return boolean to tell if the component was found
     */
    private boolean containsComponent(Component component) {
        Component[] components = container.getComponents();

        for (int i = 0; i < components.length; i++) {
            if (components[i] == component) {
                return true;
            }
        }

        return false;
    }

    /**
     * Returns the players that have a visual component and are attached.
     * The player that is denoted to be displayed first (largest) is inserted
     * as the first element in the array.
     *
     * @return an array of PlayerLayoutModel objects.
     */
    private PlayerLayoutModel[] getAttachedVisualPlayers() {
        List plList = new ArrayList(playerList.size());
        PlayerLayoutModel model = null;

        for (int i = 0; i < playerList.size(); i++) {
            model = (PlayerLayoutModel) playerList.get(i);

            if (model.isVisual() && model.isAttached()) {
                if (model.isDisplayedFirst()) {
                    plList.add(0, model);
                } else {
                    plList.add(model);
                }
            }
        }

        return (PlayerLayoutModel[]) plList.toArray(new PlayerLayoutModel[] {  });
    }

    /**
     * Calculates the optimal combination of number of columns and number of rows
     * for the specified width, height, labelheight and number of players.
     * Optimal means that the players are displayed as large as possible in the given
     * area.
     *
     * @param availableWidth the width available for the players
     * @param availableHeight the height available for the players
     * @param labelHeight the height of the label that is displayed beneath each player
     * @param numPlayers the total number of players
     * @return an int array of length 2; the first int is the number of columns,
     *   the second int is the number of rows
     */
    private int[] getNumberOfColumnsAndRows(int availableWidth,
        int availableHeight, int labelHeight, int numPlayers) {
        // calculate the average aspect ratio of the players with a visual component
        float averageAspectRatio = 0.0f;
        int visualCount = 0;

        for (int i = 0; i < playerList.size(); i++) {
            PlayerLayoutModel plModel = (PlayerLayoutModel) playerList.get(i);

            if (plModel.isVisual()) {
                averageAspectRatio += plModel.player.getAspectRatio();
                visualCount++;
            }
        }

        if (visualCount > 0) {
            averageAspectRatio /= visualCount;
        } else {
            averageAspectRatio = 1.3f; //default
        }

        if ((numPlayers == 0) || (numPlayers == 1)) {
            return new int[] { numPlayers, numPlayers };
        }

        if ((availableWidth <= 0) || (availableHeight <= 0) ||
                (availableHeight <= labelHeight)) {
            return new int[] { 0, 0 };
        }

        int maxArea = 0;
        int maxWidth = 0;
        int maxHeight = 0;
        int numColumns = 0;
        int numRows = 0;

        for (int i = 1; i <= numPlayers; i++) {
            for (int j = numPlayers; (j >= 1) && ((i * j) >= numPlayers);
                    j--) {
                maxWidth = availableWidth / i;
                maxHeight = (availableHeight - (j * labelHeight)) / j;

                if (maxHeight > (maxWidth * (1 / averageAspectRatio))) {
                    maxHeight = (int) (maxWidth * (1 / averageAspectRatio));
                }

                if (maxWidth > (maxHeight * averageAspectRatio)) {
                    maxWidth = (int) (maxHeight * averageAspectRatio);
                }

                if ((maxWidth <= 0) || (maxHeight <= 0)) {
                    continue;
                }

                int area = maxWidth * maxHeight;

                if (area > maxArea) {
                    maxArea = area;
                    numColumns = i;
                    numRows = j;
                }
            }
        }

        return new int[] { numColumns, numRows };
    }

    /**
     * Tests the media descriptors for video files and extracted audio files
     * with different offsets. When found with different offset while the video
     * is not the master media a warning message is generated.
     */
    private void checkUnequalOffsets() {
        if (playerList.size() < 2) {
            return;
        }

        StringBuffer mesBuf = null;

        MediaDescriptor amd;
        MediaDescriptor vmd;
        PlayerLayoutModel amodel;
        PlayerLayoutModel vmodel;

        for (int i = 0; i < playerList.size(); i++) {
            amodel = (PlayerLayoutModel) playerList.get(i);
            amd = amodel.player.getMediaDescriptor();

            if (amd == null) {
                continue;
            }

            if (amd.mimeType.equals(MediaDescriptor.WAV_MIME_TYPE)) {
                for (int j = 0; j < playerList.size(); j++) {
                    vmodel = (PlayerLayoutModel) playerList.get(j);
                    vmd = vmodel.player.getMediaDescriptor();

                    if (vmd == null) {
                        continue;
                    }

                    if (MediaDescriptorUtil.isVideoType(vmd)) {
                        if (vmd.mediaURL.equals(amd.extractedFrom) && (j != 0) &&
                                (vmd.timeOrigin != amd.timeOrigin)) {
                            // add to the message
                            if (mesBuf == null) {
                                mesBuf = new StringBuffer(ElanLocale.getString(
                                            "LinkedFilesDialog.Message.OffsetNotEqual") +
                                        "\n\n");
                            }

                            mesBuf.append("- " + vmd.mediaURL + "\n");
                            mesBuf.append("- " + amd.mediaURL + "\n\n");

                            break;
                        }
                    }
                }
            }
        }

        if (mesBuf != null) {
            JOptionPane.showMessageDialog(container, mesBuf.toString(),
                ElanLocale.getString("Message.Warning"),
                JOptionPane.WARNING_MESSAGE);
        }
    }

    /**
     * DOCUMENT ME!
     *
     * @return DOCUMENT ME!
     */
    public JFrame getElanFrame() {
        return elanFrame;
    }

    /**
     * Give access to the viewermanager.
     *
     * @return the viewermanager
     */
    public ViewerManager2 getViewerManager() {
        return viewerManager;
    }

    /**
     * Stores preferences/current state into a HashMap.
     *
     * @return a HashMap
     */
    public HashMap getState() {
        HashMap map = new HashMap();

        // multitier control panel
        if (multiTierControlPanel != null) {
            int sortMode = multiTierControlPanel.getSorting();
            map.put("TierSortingMode", new Integer(sortMode));
        }

        // tabpane
        if (tabPane != null) {
            map.put("SelectedTabIndex", new Integer(tabPane.getSelectedIndex()));

            for (int i = 0; i < tabPane.getTabCount(); i++) {
                SingleTierViewerPanel panel;
                JComponent comp = (JComponent) tabPane.getComponent(i);

                if (comp instanceof SingleTierViewerPanel) {
                    panel = (SingleTierViewerPanel) comp;

                    if (panel.getViewer() instanceof GridViewer) {
                        map.put("GridMultiMode",
                            new Boolean(panel.isMultiTierMode()));
                        map.put("GridTierName", panel.getSelectedTierName());
                        map.put("GridFontSize",
                            new Integer(
                                ((GridViewer) panel.getViewer()).getFontSize()));
                    } else if (panel.getViewer() instanceof TextViewer) {
                        map.put("TextTierName", panel.getSelectedTierName());
                        map.put("TextFontSize",
                            new Integer(
                                ((TextViewer) panel.getViewer()).getFontSize()));
                        map.put("TextDotSeparated",
                            new Boolean(
                                ((TextViewer) panel.getViewer()).isDotSeparated()));
                        map.put("TextCenterVertical",
                            new Boolean(
                                ((TextViewer) panel.getViewer()).isCenteredVertically()));
                    }
                } else if (comp == subtitlePanel) {
                    Component[] subComps = comp.getComponents();

                    for (int j = 0; j < subComps.length; j++) {
                        if (subComps[j] instanceof SingleTierViewerPanel) {
                            panel = (SingleTierViewerPanel) subComps[j];

                            if (panel.getViewer() instanceof SubtitleViewer) {
                                map.put(("SubTitleTierName" + j),
                                    panel.getSelectedTierName());
                                map.put(("SubTitleFontSize" + j),
                                    new Integer(
                                        ((SubtitleViewer) panel.getViewer()).getFontSize()));
                            }
                        }
                    }
                } else if (comp == controlPanel) {
                    if (mediaPlayerController != null) {
                        map.put("MediaControlVolume",
                            new Float(mediaPlayerController.getVolume()));
                        map.put("MediaControlRate",
                            new Float(mediaPlayerController.getRate()));
                    }
                }
            }
        }

        if (timeLineViewer != null) {
            map.put("TimeLineFontSize",
                new Integer(timeLineViewer.getFontSize()));

            if (showTimeLineViewer) {
                map.put("VisibleMultiTierViewer", TimeLineViewer.class.getName());
            }
        }

        if (interlinearViewer != null) {
            map.put("InterlinearFontSize",
                new Integer(interlinearViewer.getFontSize()));

            if (showInterlinearViewer) {
                map.put("VisibleMultiTierViewer",
                    InterlinearViewer.class.getName());
            }
        }

        if (timeseriesViewer != null) {
            int panelCount = timeseriesViewer.getNumberOfTrackPanels();

            if (panelCount > 0) {
                map.put("TimeSeriesNumPanels", new Integer(panelCount));

                HashMap panelMap = new HashMap(panelCount);
                String[] names;

                for (int i = 0; i < panelCount; i++) {
                    names = timeseriesViewer.getTracksForPanel(i);
                    panelMap.put(new Integer(i), names);
                }

                map.put("TimeSeriesPanelMap", panelMap);
            }
        }

        return map;
    }

    /**
     * Restores the state of the connected viewers.
     *
     * @param map a map with the stored key/value pairs
     */
    public void setState(HashMap map) {
        if (map == null) {
            return;
        }

        if (multiTierControlPanel != null) {
            Integer sortMode = (Integer) map.get("TierSortingMode");

            if (sortMode != null) {
                multiTierControlPanel.setSorting(sortMode.intValue());
            }
        }

        if (tabPane != null) {
            Integer selIndex = (Integer) map.get("SelectedTabIndex");

            if (selIndex != null) {
                int index = selIndex.intValue();

                if ((index > 0) && (index < tabPane.getTabCount())) {
                    tabPane.setSelectedIndex(index);
                }
            }

            for (int i = 0; i < tabPane.getTabCount(); i++) {
                JComponent comp = (JComponent) tabPane.getComponent(i);
                SingleTierViewerPanel panel;

                if (comp instanceof SingleTierViewerPanel) {
                    panel = (SingleTierViewerPanel) comp;

                    if (panel.getViewer() instanceof GridViewer) {
                        Boolean multiGrid = (Boolean) map.get("GridMultiMode");

                        if (multiGrid != null) {
                            panel.setMultiTierMode(multiGrid.booleanValue());
                        }

                        String tierName = (String) map.get("GridTierName");
                        panel.selectTier(tierName);

                        Integer fontSize = (Integer) map.get("GridFontSize");

                        if (fontSize != null) {
                            ((GridViewer) panel.getViewer()).setFontSize(fontSize.intValue());
                        }
                    } else if (panel.getViewer() instanceof TextViewer) {
                        String tierName = (String) map.get("TextTierName");
                        panel.selectTier(tierName);

                        Integer fontSize = (Integer) map.get("TextFontSize");

                        if (fontSize != null) {
                            ((TextViewer) panel.getViewer()).setFontSize(fontSize.intValue());
                        }

                        Boolean dotted = (Boolean) map.get("TextDotSeparated");

                        if (dotted != null) {
                            ((TextViewer) panel.getViewer()).setDotSeparated(dotted.booleanValue());
                        }

                        Boolean centered = (Boolean) map.get(
                                "TextCenterVertical");

                        if (centered != null) {
                            ((TextViewer) panel.getViewer()).setCenteredVertically(centered.booleanValue());
                        }
                    }
                } else if (comp == subtitlePanel) {
                    Component[] subComps = comp.getComponents();
                    String tierName;
                    Integer fontSize;

                    for (int j = 0; j < subComps.length; j++) {
                        if (subComps[j] instanceof SingleTierViewerPanel) {
                            panel = (SingleTierViewerPanel) subComps[j];

                            if (panel.getViewer() instanceof SubtitleViewer) {
                                tierName = (String) map.get(("SubTitleTierName" +
                                        j));
                                panel.selectTier(tierName);
                                fontSize = (Integer) map.get(("SubTitleFontSize" +
                                        j));

                                if (fontSize != null) {
                                    ((SubtitleViewer) panel.getViewer()).setFontSize(fontSize.intValue());
                                }
                            }
                        }
                    }
                } else if (comp == controlPanel) {
                    if (mediaPlayerController != null) {
                        Float volume = (Float) map.get("MediaControlVolume");

                        if (volume != null) {
                            mediaPlayerController.setVolume(volume.floatValue());
                        }

                        Float rate = (Float) map.get("MediaControlRate");

                        if (rate != null) {
                            mediaPlayerController.setRate(rate.floatValue());
                        }
                    }
                }
            }
        }

        String visViewer = (String) map.get("VisibleMultiTierViewer");

        if (timeLineViewer != null) {
            Integer fontSize = (Integer) map.get("TimeLineFontSize");

            if (fontSize != null) {
                timeLineViewer.setFontSize(fontSize.intValue());
            }

            if ((visViewer != null) &&
                    visViewer.equals(TimeLineViewer.class.getName())) {
                showTimeLineViewer();
                getMultiTierControlPanel().setViewer(timeLineViewer);
            }
        }

        if (interlinearViewer != null) {
            Integer fontSize = (Integer) map.get("InterlinearFontSize");

            if (fontSize != null) {
                interlinearViewer.setFontSize(fontSize.intValue());
            }

            if ((visViewer != null) &&
                    visViewer.equals(InterlinearViewer.class.getName())) {
                showInterlinearViewer();
                getMultiTierControlPanel().setViewer(interlinearViewer);
            }
        }

        if (timeseriesViewer != null) {
            Integer panCount = (Integer) map.get("TimeSeriesNumPanels");

            if (panCount != null) {
                int panelCount = panCount.intValue();
                timeseriesViewer.setNumberOfTrackPanels(panelCount);

                HashMap panelMap = (HashMap) map.get("TimeSeriesPanelMap");

                if (panelMap != null) {
                    Iterator panIt = panelMap.keySet().iterator();
                    Integer index;
                    String[] names;

                    while (panIt.hasNext()) {
                        index = (Integer) panIt.next();

                        if (index != null) {
                            names = (String[]) panelMap.get(index);

                            if (names != null) {
                                timeseriesViewer.setTracksForPanel(index.intValue(),
                                    names);
                            }
                        }
                    }
                }
            }
        }
    }

    /**
     * DOCUMENT ME!
     * $Id: ElanLayoutManager.java,v 1.6 2006/03/30 15:01:14 hasloe Exp $
     * @author $Author: hasloe $
     * @version $Revision: 1.6 $
     */
    private class MouseHandler extends MouseInputAdapter {
        /**
         * DOCUMENT ME!
         *
         * @param e DOCUMENT ME!
         */
        public void mouseMoved(MouseEvent e) {
            redispatchMouseEvent(e);
        }

        /**
         * DOCUMENT ME!
         *
         * @param e DOCUMENT ME!
         */
        public void mouseDragged(MouseEvent e) {
            redispatchMouseEvent(e);
        }

        /**
         * DOCUMENT ME!
         *
         * @param e DOCUMENT ME!
         */
        public void mouseClicked(MouseEvent e) {
            redispatchMouseEvent(e);
        }

        /**
         * DOCUMENT ME!
         *
         * @param e DOCUMENT ME!
         */
        public void mouseEntered(MouseEvent e) {
            redispatchMouseEvent(e);
        }

        /**
         * DOCUMENT ME!
         *
         * @param e DOCUMENT ME!
         */
        public void mouseExited(MouseEvent e) {
            redispatchMouseEvent(e);
        }

        /**
         * DOCUMENT ME!
         *
         * @param e DOCUMENT ME!
         */
        public void mousePressed(MouseEvent e) {
            redispatchMouseEvent(e);
        }

        /**
         * DOCUMENT ME!
         *
         * @param e DOCUMENT ME!
         */
        public void mouseReleased(MouseEvent e) {
            redispatchMouseEvent(e);
        }

        private void redispatchMouseEvent(MouseEvent e) {
            Point glassPanePoint = e.getPoint();
            Point containerPoint = SwingUtilities.convertPoint(glasspane,
                    glassPanePoint, container);
            Component component = SwingUtilities.getDeepestComponentAt(container,
                    containerPoint.x, containerPoint.y);
            JMenuBar menuBar = elanFrame.getJMenuBar();

            if (containerPoint.y < 0) { //we're not in the content pane

                if ((containerPoint.y + menuBar.getHeight()) >= 0) {
                    //The mouse event is over the menu bar.
                    //make glasspane a little smaller, so it isn't in front of the menu from elanframe2
                    //do this everytime, so the glasspane resizes when elanframe resizes
                    Dimension dim = elanFrame.getSize();
                    int glassPaneMoveYDirection = (int) menuBar.getSize()
                                                               .getHeight();
                    glasspane.setSize((int) dim.getWidth(),
                        ((int) dim.getHeight() - glassPaneMoveYDirection));
                    glasspane.setLocation(0, glassPaneMoveYDirection);
                } else {
                    //The mouse event is over non-system window
                    //decorations, such as the ones provided by
                    //the Java look and feel.
                    //Could handle specially.
                }
            } else {
                //The mouse event is probably over the content pane.
                if (component != null) {
                    //if ( (component != null) && (component.equals(timeLineViewer)) ) {
                    //                   Point componentPoint = SwingUtilities.convertPoint(glasspane, glassPanePoint, component);
                    //                    component.dispatchEvent(new MouseEvent(component, e.getID(), e.getWhen(), e.getModifiers(), componentPoint.x, componentPoint.y, e.getClickCount(), e.isPopupTrigger()));
                }
            }
        }
    }

    /**
     * Listener for  ElanFrame ComponentEvents
     */
    private class ContainerComponentListener extends ComponentAdapter {
        /**
         * Calls doLayout() after a change in the size of the content pane
         * of the ElanFrame.
         *
         * @param e component event
         */
        public void componentResized(ComponentEvent e) {
            doLayout();
        }
    }

    /**
     * A special component listener for the splitpane that only lays out the
     * viewers and panels in the splitpane when the divider has been relocated
     * (dragged). The componentResized() method does not simply call doLayout()
     * because the double call to doLayout() after a resize of the main container
     * (the contentpane of the Elan frame) messed up the layout of some components.
     * This listener is only needed when ther is a top component as well as a bottom
     * component in the splitpane (i.e. when the divider can be dragged).
     *
     * @author Han Sloetjes
     */
    private class SignalSplitPaneListener extends ComponentAdapter {
        /**
         * Sets the bounds of the components in the splitpane, when and only
         * when their height is not equal to the height of their parent
         * in the enclosing splitpane area.
         *
         * @param e the component event
         */
        public void componentResized(ComponentEvent e) {
            if ((e == null) || (e.getComponent() == null)) {
                return;
            }

            if (timeLineSplitPane != null) {
                Component top = timeLineSplitPane.getTopComponent();

                if ((top != null) && top instanceof Container) {
                    int height = top.getHeight();
                    Component[] cc = ((Container) top).getComponents();

                    for (int i = 0; i < cc.length; i++) {
                        if (cc[i].getHeight() != height) {
                            cc[i].setSize(cc[i].getWidth(), height);
                        }
                    }
                }

                Component bottom = timeLineSplitPane.getBottomComponent();

                if ((bottom != null) && bottom instanceof Container) {
                    int height = bottom.getHeight();
                    Component[] cc = ((Container) bottom).getComponents();

                    for (int i = 0; i < cc.length; i++) {
                        if (cc[i].getHeight() != height) {
                            cc[i].setSize(cc[i].getWidth(), height);
                        }
                    }
                }
            }
        }
    }

    /**
     * A class for convenient storage of layout related attributes,
     * to avoid we have to administer a number of lists of attributes for players.
     *
     * @author Han Sloetjes
     */
    private class PlayerLayoutModel {
        final ElanMediaPlayer player;
        private boolean attached;
        private DetachedFrame detachedFrame;
        Component visualComponent;
        private boolean displayedFirst;
        private boolean syncOnly = false;

        /**
         * Constructor. The player should not be null.
         *
         * @param player the player
         */
        PlayerLayoutModel(ElanMediaPlayer player) {
            this.player = player;
            visualComponent = player.getVisualComponent();
            attached = true;
            detachedFrame = null;
            displayedFirst = false;
        }

        /**
         * Changes the flag that determines whether or not this player should
         * be displayed as the first (and largest) of all attached videos
         * (if this player has a visual component).
         *
         * @param first when true this video will be given the first position
         *  and will get the largest area for its visual component
         */
        public void setDisplayedFirst(boolean first) {
            displayedFirst = first;
        }

        /**
         * Returns whether or not this player is marked as the one to be displayed
         * as the first (and the largest) of the attached players.
         *
         * @return true if this player is to be given the first position (and the
         * largest area for its visual component) of the attached players
         */
        public boolean isDisplayedFirst() {
            return displayedFirst;
        }

        /**
         * Returns the attached/detached state.
         * @return the attached/detached state
         */
        boolean isAttached() {
            return attached;
        }

        /**
         * Returns whether this player has a (meaningful) visual component.
         * The EmptyMediaPlayer only has a dummy component that is not to be
         * added to any layout.
         *
         * @return whether this player has a (meaningful) visual component
         */
        boolean isVisual() {
            if (player instanceof EmptyMediaPlayer) {
                return false;
            } else {
                return visualComponent != null;
            }
        }

        /**
         * Detaches the player. The visual compnent is added to the content pane
         * of its own Frame.
         */
        void detach() {
            if (!attached || !isVisual()) {
                return;
            }

            if (player instanceof QTMediaPlayer) {
                visualComponent = ((QTMediaPlayer) player).createNewVisualComponent();
            }

            detachedFrame = new DetachedFrame(ElanLayoutManager.this,
                    visualComponent, player.getMediaDescriptor().mediaURL);
            detachedFrame.setAspectRatio(player.getAspectRatio());

            detachedFrame.setSize(400, 400);
            detachedFrame.setVisible(true);

            attached = false;
        }

        /**
         * Removes the visual component from the content pane of a detached frame
         * and disposes the frame.
         * Then the visual component can then be added to the content pane of the main
         * application frame. This is not done here.
         */
        void attach() {
            if (attached || !isVisual() || (detachedFrame == null)) {
                return;
            }

            detachedFrame.getContentPane().remove(visualComponent);
            detachedFrame.setVisible(false);
            detachedFrame.dispose();
            detachedFrame = null;
            attached = true;

            if (player instanceof QTMediaPlayer) {
                visualComponent = ((QTMediaPlayer) player).createNewVisualComponent();
            }
        }
    }
}
