/*
 * File:     TimeLineViewer.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.viewer;

import mpi.eudico.client.annotator.Constants;
import mpi.eudico.client.annotator.ElanLocale;

import mpi.eudico.client.annotator.commands.Command;
import mpi.eudico.client.annotator.commands.ELANCommandFactory;

import mpi.eudico.client.annotator.gui.EditTierDialog;
import mpi.eudico.client.annotator.gui.InlineEditBox;

import mpi.eudico.client.annotator.util.Tag2D;
import mpi.eudico.client.annotator.util.Tier2D;

import mpi.eudico.client.mediacontrol.ControllerEvent;
import mpi.eudico.client.mediacontrol.StopEvent;
import mpi.eudico.client.mediacontrol.TimeEvent;

import mpi.eudico.client.util.TimeFormatter;

import mpi.eudico.server.corpora.clom.Annotation;
import mpi.eudico.server.corpora.clom.Tier;
import mpi.eudico.server.corpora.clom.TimeSlot;
import mpi.eudico.server.corpora.clom.Transcription;

import mpi.eudico.server.corpora.clomimpl.abstr.AlignableAnnotation;
import mpi.eudico.server.corpora.clomimpl.abstr.TierImpl;
import mpi.eudico.server.corpora.clomimpl.type.Constraint;
import mpi.eudico.server.corpora.clomimpl.type.LinguisticType;

import mpi.eudico.server.corpora.util.ACMEditEvent;
import mpi.eudico.server.corpora.util.ACMEditListener;

import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Toolkit;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.AdjustmentEvent;
import java.awt.event.AdjustmentListener;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;

import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Vector;

import javax.swing.ButtonGroup;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JRadioButtonMenuItem;
import javax.swing.JScrollBar;
import javax.swing.JToolTip;
import javax.swing.KeyStroke;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.ToolTipManager;
import javax.swing.UIManager;


/**
 * This viewer shows annotations of multiple tiers relative to a time scale.<br>
 * The value of each tag is truncated in such a way that it does not extent
 * beyond the available space at a given resolution.
 *
 * @author Han Sloetjes
 * @version 0.1 2/7/2003
 * @version Aug 2005 Identity removed
 */
public class TimeLineViewer extends TimeScaleBasedViewer
    implements ComponentListener, MouseListener, MouseMotionListener,
        MouseWheelListener, KeyListener, AdjustmentListener, ActionListener,
        MultiTierViewer, ACMEditListener {
    /** default number of pixels that represents one second */
    static final int PIXELS_FOR_SECOND = 100;
    private Transcription transcription;
    private boolean rulerAlwaysVisible;
    private int rulerHeight;
    private TimeRuler ruler;
    private Font font;
    private Font tooltipFont;
    private FontMetrics metrics;
    private BufferedImage bi;
    private Graphics2D big2d;
    private AlphaComposite alpha04;
    private AlphaComposite alpha07;
    private BasicStroke stroke;
    private BasicStroke stroke2;
    private BasicStroke stroke3;
    private int msPerPixel;

    /** default value of milliseconds per pixel */
    public final int DEFAULT_MS_PER_PIXEL = 10;

    /**
     * The resolution in number of pixels for a second. This is not a
     * percentage value. Historically resolution = PIXELS_FOR_SECOND  factor,
     * where factor = 100 / menu_resolution_percentage_value.
     */
    private int resolution;
    private int imageWidth;
    private int imageHeight;
    private long crossHairTime;
    private int crossHairPos;
    private long intervalBeginTime;
    private long intervalEndTime;
    private int verticalScrollOffset;
    private long selectionBeginTime;
    private long selectionEndTime;
    private int selectionBeginPos;
    private int selectionEndPos;
    private long dragStartTime;
    private Point dragStartPoint;
    private Point dragEndPoint;

    /** width of border area where auto scrolling starts */
    public final int SCROLL_OFFSET = 16;
    private DragScroller scroller;
    private JPopupMenu popup;
    private ButtonGroup zoomBG;
    private JMenu zoomMI;
    private ButtonGroup fontSizeBG;
    private JMenu fontMenu;
    private JCheckBoxMenuItem timeScaleConMI;
    private JCheckBoxMenuItem activeAnnStrokeBoldMI;

    // menu items that can be enabled / disabled
    private JMenuItem newAnnoMI;
    private JMenuItem newAnnoBeforeMI;
    private JMenuItem newAnnoAfterMI;
    private JMenuItem modifyAnnoMI;
    private JMenuItem modifyAnnoTimeMI;
    private JMenuItem deleteAnnoMI;
    private JMenuItem activeTierMI;
    private JMenuItem deleteTierMI;
    private JMenuItem changeTierMI;
    private boolean timeScaleConnected;
    private boolean panMode;

    // do or don't show empty slots on a child tier
    private boolean showEmptySlots;
    private boolean aaStrokeBold;

    /** Holds value of property DOCUMENT ME! */
    protected int pixelsForTierHeight;

    /** Holds value of property DOCUMENT ME! */
    protected int pixelsForTierHeightMargin;

    //new storage fields
    private ArrayList allTiers;
    private List visibleTiers;
    private Tag2D hoverTag2D;
    private int hoverTierIndex;

    /** Holds value of property DOCUMENT ME! */
    protected Tag2D cursorTag2D;
    private int cursorTierIndex;
    private Tier2D rightClickTier;
    private long rightClickTime;

    // vertical scrolling
    private JScrollBar scrollBar;

    /** default scrollbar width */
    private final int defBarWidth;
    private int[] tierYPositions;
    private int tooltipFontSize;

    /** ar the control panel that receives the setTierPositions call */
    MultiTierControlPanel multiTierControlPanel;

    // editing
    private InlineEditBox editBox;
    private boolean forceOpenControlledVocabulary = false;
    private Tag2D dragEditTag2D;
    private boolean dragEditing;
    private Color dragEditColor = Color.green;

    /** Holds value of property DOCUMENT ME! */
    private final int DRAG_EDIT_MARGIN = 8;

    /** Holds value of property DOCUMENT ME! */
    private final int DRAG_EDIT_CENTER = 0;

    /** Holds value of property DOCUMENT ME! */
    private final int DRAG_EDIT_LEFT = 1;

    /** Holds value of property DOCUMENT ME! */
    private final int DRAG_EDIT_RIGHT = 2;
    private int dragEditMode = 0;

    // the parent's boundaries
    private long dragParentBegin = -1L;
    private long dragParentEnd = -1L;

    // a flag for the scroll thread

    /** Holds value of property DOCUMENT ME! */
    boolean stopScrolling = true;
    private Object tierLock = new Object();

    /**
     * Constructs a new TimeLineViewer.<br>
     * Takes care of some one time initialization and adds listeners.
     */
    public TimeLineViewer() {
        initViewer();
        defBarWidth = getDefaultBarWidth();
        addComponentListener(this);
        addMouseListener(this);
        addMouseMotionListener(this);
        addMouseWheelListener(this);
        addKeyListener(this);
        setDoubleBuffered(true);
        setOpaque(true);
    }

    /**
     * Constructs a new TimeLineViewer using the specified transcription.<br>
     * Calls the no-arg constructor first.
     *
     * @param transcription the transcription containing the data for the
     *        viewer
     */
    public TimeLineViewer(Transcription transcription) {
        this();
        this.transcription = transcription;
        paintBuffer();
        initTiers();
    }

    /**
     * Overrides <code>JComponent</code>'s processKeyBinding by always
     * returning false. Necessary for the proper working of (menu) shortcuts
     * in Elan.
     *
     * @param ks DOCUMENT ME!
     * @param e DOCUMENT ME!
     * @param condition DOCUMENT ME!
     * @param pressed DOCUMENT ME!
     *
     * @return DOCUMENT ME!
     */
    protected boolean processKeyBinding(KeyStroke ks, KeyEvent e,
        int condition, boolean pressed) {
        return false;
    }

    /**
     * Performs the initialization of fields and sets up the viewer.<br>
     */
    private void initViewer() {
        font = Constants.DEFAULTFONT;
        setFont(font);
        metrics = getFontMetrics(font);
        tooltipFontSize = getDefaultTooltipFontSize();
        tooltipFont = font.deriveFont((float) tooltipFontSize);

        // Keep the tool tip showing
        int dismissDelay = Integer.MAX_VALUE;
        ToolTipManager.sharedInstance().setDismissDelay(dismissDelay);

        rulerAlwaysVisible = true;
        ruler = new TimeRuler(font, TimeFormatter.toString(0));
        rulerHeight = ruler.getHeight();
        stroke = new BasicStroke();
        stroke2 = new BasicStroke(2.0f);
        stroke3 = new BasicStroke(3.0f);
        msPerPixel = 10;
        resolution = PIXELS_FOR_SECOND;
        crossHairTime = 0L;
        crossHairPos = 0;
        intervalBeginTime = 0L;
        intervalEndTime = 0L;
        verticalScrollOffset = 0;

        selectionBeginTime = 0L;
        selectionEndTime = 0L;
        selectionBeginPos = 0;
        selectionEndPos = 0;
        dragStartTime = 0;
        timeScaleConnected = true;

        imageWidth = 0;
        imageHeight = 0;
        alpha04 = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.4f);
        alpha07 = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.7f);

        //pixelsForTierHeight = font.getSize() * 3; //hardcoded for now
        pixelsForTierHeight = font.getSize() + 24;
        pixelsForTierHeightMargin = 2; // hardcoded for now

        scrollBar = new JScrollBar(JScrollBar.VERTICAL, 0, 50, 0, 200);
        scrollBar.setBlockIncrement(pixelsForTierHeight);
        scrollBar.addAdjustmentListener(this);

        //setLayout(new BorderLayout());
        //add(scrollBar, BorderLayout.EAST);
        setLayout(null);
        add(scrollBar);

        editBox = new InlineEditBox(true);
        editBox.setFont(font);
        editBox.setVisible(false);
        add(editBox);
    }

    /**
     * Retrieves the default, platform specific width of a scrollbar.
     *
     * @return the default width, or 20 when not found
     */
    private int getDefaultBarWidth() {
        int width = 20;

        if (UIManager.getDefaults().get("ScrollBar.width") != null) {
            width = ((Integer) (UIManager.getDefaults().get("ScrollBar.width"))).intValue();
        }

        return width;
    }

    /**
     * Initialise tiers and tags.
     */
    private void initTiers() {
        allTiers = new ArrayList(20);
        visibleTiers = new ArrayList(allTiers.size());

        if (transcription == null) {
            return;
        }

        extractTiers();
        tierYPositions = new int[allTiers.size()];

        //allTiers is filled, set all tiers visible
        // not neccessary anymore

        /*
           Iterator it = allTiers.iterator();
           while(it.hasNext()) {
               visibleTiers.add(it.next());
           }
         */
    }

    /**
     * Extract all Tiers from the Transcription. Store the information in
     * Tier2D and Tag2D objects.
     */
    private void extractTiers() {
        Tier2D tier2d;

        Iterator tierIter = transcription.getTiers().iterator();

        while (tierIter.hasNext()) {
            TierImpl tier = (TierImpl) tierIter.next();
            tier2d = createTier2D(tier);

            allTiers.add(tier2d);
        }
    }

    private Tier2D createTier2D(TierImpl tier) {
        Tier2D tier2d = new Tier2D(tier);
        Tag2D tag2d;
        int xPos;
        int tagWidth;

        Iterator annotIter = tier.getAnnotations().iterator();

        while (annotIter.hasNext()) {
            Annotation a = (Annotation) annotIter.next();

            //System.out.println("Annotation: " + a);
            tag2d = new Tag2D(a);
            xPos = timeToPixels(a.getBeginTimeBoundary());
            tag2d.setX(xPos);
            tagWidth = timeToPixels(a.getEndTimeBoundary()) - xPos;
            tag2d.setWidth(tagWidth);
            tag2d.setTruncatedValue(truncateString(a.getValue(), tagWidth,
                    metrics));
            tier2d.addTag(tag2d);

            if (a == getActiveAnnotation()) {
                cursorTag2D = tag2d;
            }
        }

        return tier2d;
    }

    /**
     * When the resolution or zoom level of the viewer has been changed the
     * Tag2D x position, width and truncated string value needs to be
     * recalculated.
     */
    private void recalculateTagSizes() {
        Tier2D tier2d;
        Tag2D tag2d;
        int xPos;
        int tagWidth;
        Iterator tierIt = allTiers.iterator();

        while (tierIt.hasNext()) {
            tier2d = (Tier2D) tierIt.next();

            Iterator tagIt = tier2d.getTags();

            while (tagIt.hasNext()) {
                tag2d = (Tag2D) tagIt.next();
                xPos = timeToPixels(tag2d.getBeginTime());
                tag2d.setX(xPos);
                tagWidth = timeToPixels(tag2d.getEndTime()) - xPos;
                tag2d.setWidth(tagWidth);
                tag2d.setTruncatedValue(truncateString(tag2d.getValue(),
                        tagWidth, metrics));
            }
        }
    }

    /**
     * Re-processes the annotations of a tier.<br>
     * Necessary after removal of an unknown number of annotations.
     *
     * @param tier2d the Tier2D
     */
    private void reextractTagsForTier(Tier2D tier2d) {
        if ((transcription == null) || (tier2d == null)) {
            return;
        }

        //int index = transcription.getTiers(userIdentity).indexOf(tier2d.getTier());
        //TierImpl tier = null;
        //if (index > -1) {
        //    tier = (TierImpl) transcription.getTiers(userIdentity).get(index);
        //}
        TierImpl tier = tier2d.getTier();

        if (tier == null) {
            return;
        }

        tier2d.getTagsList().clear();

        Tag2D tag2d;
        int xPos;
        int tagWidth;
        Iterator annotIter = tier.getAnnotations().iterator();

        while (annotIter.hasNext()) {
            Annotation a = (Annotation) annotIter.next();

            //System.out.println("Annotation: " + a);
            tag2d = new Tag2D(a);
            xPos = timeToPixels(a.getBeginTimeBoundary());
            tag2d.setX(xPos);
            tagWidth = timeToPixels(a.getEndTimeBoundary()) - xPos;
            tag2d.setWidth(tagWidth);
            tag2d.setTruncatedValue(truncateString(a.getValue(), tagWidth,
                    metrics));
            tier2d.addTag(tag2d);

            if (a == getActiveAnnotation()) {
                cursorTag2D = tag2d;
            }
        }
    }

    /**
     * Create a truncated String of a tag's value to display in the viewer.
     *
     * @param string the tag's value
     * @param width the available width for the String
     * @param fMetrics the font metrics
     *
     * @return the truncated String
     */
    private String truncateString(String string, int width, FontMetrics fMetrics) {
        String line = string.replace('\n', ' ');

        if (fMetrics != null) {
            int stringWidth = fMetrics.stringWidth(line);

            if (stringWidth > (width - 4)) { // truncate

                int i = 0;
                String s = "";
                int size = line.length();

                while (i < size) {
                    if (fMetrics.stringWidth(s) > (width - 4)) {
                        break;
                    } else {
                        s = s + line.charAt(i++);
                    }
                }

                if (!s.equals("")) {
                    line = s.substring(0, s.length() - 1);
                } else {
                    line = s;
                }
            }
        }

        return line;
    }

    /**
     * Paint to a buffer.<br>
     * First paint the top ruler, next the current selection and finally paint
     * the tags of the visible tiers.
     */
    private void paintBuffer() {
        if ((getWidth() <= 0) || (getHeight() <= 0)) {
            return;
        }

        /*
           int barWidth = scrollBar.getWidth();
           if (barWidth <= 0) {
               if (UIManager.getDefaults().get("ScrollBar.width") != null) {
                   barWidth = ((Integer)(UIManager.getDefaults().get("ScrollBar.width"))).intValue();
               } else {
                   barWidth = 0;
               }
           }
         */
        if (((getWidth() - defBarWidth) != imageWidth) ||
                (imageHeight != ((visibleTiers.size() * pixelsForTierHeight) +
                rulerHeight))) {
            imageWidth = getWidth() - defBarWidth;
            imageHeight = (visibleTiers.size() * pixelsForTierHeight) +
                rulerHeight;

            if ((imageWidth <= 0) || (imageHeight <= 0)) {
                return;
            }

            //bi = new BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_INT_RGB);
            //big2d = bi.createGraphics();
            //if (intervalEndTime == 0) {
            intervalEndTime = intervalBeginTime +
                (int) (imageWidth * msPerPixel);

            if (timeScaleConnected) {
                setGlobalTimeScaleIntervalEndTime(intervalEndTime);
            }

            //}
        }

        if ((bi == null) || (bi.getWidth() < imageWidth) ||
                (bi.getHeight() < imageHeight)) {
            bi = new BufferedImage(imageWidth, imageHeight,
                    BufferedImage.TYPE_INT_RGB);
            big2d = bi.createGraphics();
        }

        //big2d.setFont(font);
        big2d.setColor(Constants.DEFAULTBACKGROUNDCOLOR);
        big2d.fillRect(0, 0, imageWidth, bi.getHeight());

        // mark the area beyond the media time
        if (intervalEndTime > getMediaDuration()) {
            int xx = xAt(getMediaDuration());
            big2d.setColor(UIManager.getColor("Panel.background"));
            big2d.fillRect(xx, 0, imageWidth - xx, bi.getHeight());
        }

        /*paint time ruler */
        big2d.setColor(Constants.DEFAULTFOREGROUNDCOLOR);
        big2d.translate(-(intervalBeginTime / msPerPixel), 0.0);
        ruler.paint(big2d, intervalBeginTime, imageWidth, msPerPixel,
            SwingConstants.TOP);
        big2d.setFont(font);

        ///end ruler
        //paint selection
        if (selectionBeginPos != selectionEndPos) {
            int beginPos = timeToPixels(getSelectionBeginTime());
            int endPos = timeToPixels(getSelectionEndTime());
            big2d.setColor(Constants.SELECTIONCOLOR);
            big2d.setComposite(alpha04);
            big2d.fillRect(beginPos, 0, (endPos - beginPos), rulerHeight);
            big2d.setComposite(AlphaComposite.Src);
            big2d.fillRect(beginPos, rulerHeight, (endPos - beginPos),
                imageHeight - rulerHeight);
        }

        // paint tags
        int y = rulerHeight;

        //int x;
        //int w;
        int h = pixelsForTierHeight - (2 * pixelsForTierHeightMargin);

        Tier2D tier2d;
        Tag2D tag2d;

        synchronized (tierLock) {
            Iterator visIt = visibleTiers.iterator();

            while (visIt.hasNext()) {
                tier2d = (Tier2D) visIt.next();

                if (tier2d.isActive()) {
                    int ax = timeToPixels(intervalBeginTime);
                    big2d.setColor(Constants.ACTIVETIERCOLOR);
                    big2d.setComposite(alpha07);
                    big2d.fillRect(ax, y, imageWidth, pixelsForTierHeight);
                    big2d.setComposite(AlphaComposite.Src);
                }

                Iterator tagIt = tier2d.getTags();

                while (tagIt.hasNext()) {
                    tag2d = (Tag2D) tagIt.next();

                    if (tag2d.getEndTime() < intervalBeginTime) {
                        continue; //don't paint
                    } else if (tag2d.getBeginTime() > intervalEndTime) {
                        break; // stop looping this tier
                    }

                    //x = timeToPixels(tag2d.getBeginTime());
                    //w = timeToPixels(tag2d.getEndTime()) - x;
                    //paint tag at this position
                    paintTag(big2d, tag2d, tag2d.getX(),
                        y + pixelsForTierHeightMargin, tag2d.getWidth(), h);
                }

                y += pixelsForTierHeight;
            }
        }

        // end paint tags
        big2d.setTransform(new AffineTransform()); //reset transform

        //big2d.dispose(); // does not work properly in jdk 1.4
        repaint();
    }

    /**
     * Override <code>JComponent</code>'s paintComponent to paint:<br>
     * - a BufferedImage with a ruler, the selection and the tags<br>
     * - the current selection Tag<br>
     * - the "mouse over" Tag<br>
     * - the time ruler when rulerAlwaysVisible is true<br>
     * - the cursor / crosshair - empty slots - the drag edit tag
     *
     * @param g the graphics object
     */
    public void paintComponent(Graphics g) {
        super.paintComponent(g);

        Graphics2D g2d = (Graphics2D) g;
        int h = getHeight();

        // scrolling related fill
        g2d.setColor(Constants.DEFAULTBACKGROUNDCOLOR);
        g2d.fillRect(0, 0, imageWidth, h);

        if (bi != null) {
            g2d.translate(0, -verticalScrollOffset);
            g2d.drawImage(bi, 0, 0, this);
            g2d.translate(0, verticalScrollOffset);
        }

        g2d.setFont(font);

        /* don't paint the hoverTag for now
           if (hoverTag2D != null) {
               //System.out.println("tag: " + hoverTag2D);
               int x = xAt(hoverTag2D.getBeginTime());
               int w = xAt(hoverTag2D.getEndTime()) - x;
               int y = (rulerHeight + hoverTierIndex * pixelsForTierHeight + pixelsForTierHeightMargin) - verticalScrollOffset;
               int he = pixelsForTierHeight - 2 * pixelsForTierHeightMargin;
               paintHoverTag2D(g2d, hoverTag2D, x, y, w, he);
           }
         */
        if ((cursorTag2D != null) &&
                visibleTiers.contains(cursorTag2D.getTier2D())) {
            //int x = xAt(cursorTag2D.getBeginTime());
            //int w = xAt(cursorTag2D.getEndTime()) - x;
            int x = (int) ((cursorTag2D.getBeginTime() / msPerPixel) -
                (intervalBeginTime / msPerPixel));
            int w = (int) ((cursorTag2D.getEndTime() -
                cursorTag2D.getBeginTime()) / msPerPixel);
            int y = (rulerHeight + (cursorTierIndex * pixelsForTierHeight) +
                pixelsForTierHeightMargin) - verticalScrollOffset;
            int he = pixelsForTierHeight - (2 * pixelsForTierHeightMargin);
            paintCursorTag2D(g2d, cursorTag2D, x, y, w, he);
        }

        //paint empty slots
        if (showEmptySlots) {
            for (int i = 0; i < visibleTiers.size(); i++) {
                TierImpl ti = ((Tier2D) visibleTiers.get(i)).getTier();

                if (ti.getParentTier() == null) {
                    continue;
                } else {
                    if (!ti.isTimeAlignable()) {
                        int y = (rulerHeight + (i * pixelsForTierHeight) +
                            pixelsForTierHeightMargin) - verticalScrollOffset;
                        int he = pixelsForTierHeight -
                            (2 * pixelsForTierHeightMargin);
                        paintEmptySlots(g2d, ti, y, he);
                    }
                }
            }
        }

        // paint the dragEdit annotation
        if (dragEditTag2D != null) {
            //long newTime = pixelToTime(dragEditTag2D.getX());
            //int x = (int) ((dragEditTag2D.getBeginTime() / msPerPixel) -
            //	(intervalBeginTime / msPerPixel));
            int x = (int) (dragEditTag2D.getX() -
                (intervalBeginTime / msPerPixel));

            //int w = (int) ((dragEditTag2D.getEndTime() -
            //dragEditTag2D.getBeginTime()) / msPerPixel);
            int w = dragEditTag2D.getWidth();
            int y = (rulerHeight + (cursorTierIndex * pixelsForTierHeight) +
                
                /*(getTierIndexForAnnotation(dragEditTag2D.getAnnotation()) * pixelsForTierHeight) +*/
                pixelsForTierHeightMargin) - verticalScrollOffset;
            int he = pixelsForTierHeight - (2 * pixelsForTierHeightMargin);
            paintDragEditTag2D(g2d, x, y, w, he);
        }

        if (rulerAlwaysVisible && (bi != null)) {
            g2d.setClip(0, 0, imageWidth, rulerHeight);
            g2d.drawImage(bi, 0, 0, this);
            g2d.setClip(null);
            g2d.setColor(Constants.SELECTIONCOLOR);
            g2d.drawLine(0, rulerHeight, imageWidth, rulerHeight);
        }

        if ((crossHairPos >= 0) && (crossHairPos <= imageWidth)) {
            // prevents drawing outside the component on Mac
            g2d.setColor(Constants.CROSSHAIRCOLOR);
            g2d.drawLine(crossHairPos, 0, crossHairPos, h);
        }
    }

    /**
     * Paint the given Tag2D to the specified Graphics2D object using the
     * specified location and dimension.
     *
     * @param g2d the graphics object to paint to
     * @param tag2d the tag to paint
     * @param x the x postion of the tag
     * @param y the y position of the tag
     * @param width the width of the tag
     * @param height the height of the tag
     */
    private void paintTag(Graphics2D g2d, Tag2D tag2d, int x, int y, int width,
        int height) {
        if (tag2d.getAnnotation() instanceof AlignableAnnotation) {
            AlignableAnnotation a = (AlignableAnnotation) tag2d.getAnnotation();
            TimeSlot b = a.getBegin();
            TimeSlot e = a.getEnd();

            //skip check cursor
            g2d.setColor(Constants.DEFAULTFOREGROUNDCOLOR);
            g2d.drawLine(x, y + (b.isTimeAligned() ? 0 : (height / 4)), x,
                y + (b.isTimeAligned() ? height : ((height * 3) / 4)));
            g2d.drawLine(x, y + (height / 2), x + width, y + (height / 2));
            g2d.drawLine(x + width, y + (e.isTimeAligned() ? 0 : (height / 4)),
                x + width, y +
                (e.isTimeAligned() ? height : ((height * 3) / 4)));
        } else {
            //not alignable
            g2d.setColor(Constants.SHAREDCOLOR1);
            g2d.drawLine(x, y + (height / 4), x, y + ((height * 3) / 4));
            g2d.drawLine(x, y + (height / 2), x + width, y + (height / 2));
            g2d.drawLine(x + width, y + (height / 4), x + width,
                y + ((height * 3) / 4));
        }

        g2d.setColor(Constants.DEFAULTFOREGROUNDCOLOR);

        int descent = g2d.getFontMetrics().getDescent();
        g2d.drawString(tag2d.getTruncatedValue(), (float) (x + 4),
            (float) (y + ((height / 2) - descent + 1)));
    }

    /**
     * Paint the mouseover highlight for a tag.
     *
     * @param g2d
     * @param tag2d
     * @param x
     * @param y
     * @param width
     * @param height
     */

    /*
       private void paintHoverTag2D(Graphics2D g2d, Tag2D tag2d, int x, int y,
           int width, int height) {
           g2d.setColor(Constants.SHAREDCOLOR3);
           g2d.drawRect(x, y, width, height);
           g2d.setColor(Constants.SHAREDCOLOR4);
           g2d.fillRect(x, y, width - 1, height - 1);
           g2d.setColor(Constants.DEFAULTFOREGROUNDCOLOR);
           g2d.drawString(tag2d.getTruncatedValue(), x + 4,
               (int) (y + ((height / 2) - 1)));
       }
     */

    /**
     * Paint the selected Tag.
     *
     * @param g2d
     * @param tag2d
     * @param x
     * @param y
     * @param width
     * @param height
     */
    private void paintCursorTag2D(Graphics2D g2d, Tag2D tag2d, int x, int y,
        int width, int height) {
        if (tag2d.getAnnotation() instanceof AlignableAnnotation) {
            AlignableAnnotation a = (AlignableAnnotation) tag2d.getAnnotation();
            TimeSlot b = a.getBegin();
            TimeSlot e = a.getEnd();
            g2d.setColor(Constants.ACTIVEANNOTATIONCOLOR);

            if (aaStrokeBold) {
                g2d.setStroke(stroke2);
            }

            int top = b.isTimeAligned() ? 0 : (height / 4);
            int bottom = b.isTimeAligned() ? height : ((height * 3) / 4);
            g2d.drawLine(x, y + top, x, y + bottom);
            top = height / 2;

            if (aaStrokeBold) {
                top++;
            }

            //g2d.drawLine(x, y + top + 1, x + width, y + top + 1);
            g2d.drawLine(x, y + top, x + width, y + top);
            top = e.isTimeAligned() ? 0 : (height / 4);
            bottom = e.isTimeAligned() ? height : ((height * 3) / 4);
            g2d.drawLine(x + width, y + top, x + width, y + bottom);
            g2d.setStroke(stroke);
        } else {
            //not alignable
            g2d.setColor(Constants.ACTIVEANNOTATIONCOLOR);

            if (aaStrokeBold) {
                g2d.setStroke(stroke2);
            }

            g2d.drawLine(x, y + (height / 4), x, y + ((height * 3) / 4));
            g2d.drawLine(x, y + (height / 2), x + width, y + (height / 2));
            g2d.drawLine(x + width, y + (height / 4), x + width,
                y + ((height * 3) / 4));
            g2d.setStroke(stroke);
        }
    }

    /**
     * Paints the tag is edited by dragging its boundaries, or by dragging  the
     * whole tag.
     *
     * @param g2d
     * @param x
     * @param y
     * @param width
     * @param height
     */
    private void paintDragEditTag2D(Graphics2D g2d, int x, int y, int width,
        int height) {
        g2d.setColor(dragEditColor);

        if (aaStrokeBold) {
            g2d.setStroke(stroke2);
        }

        int top = 0;
        int bottom = height;
        g2d.drawLine(x, y + top, x, y + bottom);
        top = height / 2;

        if (aaStrokeBold) {
            top++;
        }

        //g2d.drawLine(x, y + top + 1, x + width, y + top + 1);
        g2d.drawLine(x, y + top, x + width, y + top);

        g2d.drawLine(x + width, y, x + width, y + bottom);
        g2d.setStroke(stroke);
    }

    /**
     * Paint empty slots on this tier.<br>
     * Iterate over the parent tags in the visible area and paint a tag when
     * it is not on the child.
     *
     * @param g2d the graphics context
     * @param ti the tier containing empty slots
     * @param y y coordinate for the tags
     * @param he height of the tags
     */
    private void paintEmptySlots(Graphics2D g2d, TierImpl ti, int y, int he) {
        try {
            TierImpl parent = (TierImpl) ti.getParentTier();
            Vector tags = parent.getAnnotations();
            Annotation a;
            int x;
            int wi;
            Iterator tagIt = tags.iterator();

            while (tagIt.hasNext()) {
                a = (Annotation) tagIt.next();

                if (a.getEndTimeBoundary() < intervalBeginTime) {
                    continue;
                }

                if (a.getBeginTimeBoundary() > intervalEndTime) {
                    break;
                }

                if (a.getChildrenOnTier(ti).size() == 0) {
                    x = (int) ((a.getBeginTimeBoundary() / msPerPixel) -
                        (intervalBeginTime / msPerPixel));
                    wi = (int) ((a.getEndTimeBoundary() -
                        a.getBeginTimeBoundary()) / msPerPixel);
                    g2d.setColor(Constants.SHAREDCOLOR4);
                    g2d.fillRect(x, y, wi, he);
                    g2d.setColor(Constants.DEFAULTFOREGROUNDCOLOR);
                    g2d.drawRect(x, y, wi, he);
                }
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    /**
     * Returns the x-ccordinate for a specific time. The coordinate is in the
     * component's coordinate system.
     *
     * @param t time
     *
     * @return int the x-coordinate for the specified time
     */
    public int xAt(long t) {
        return (int) ((t - intervalBeginTime) / msPerPixel);
    }

    /**
     * Returns the time in ms at a given position in the current image. The
     * given x coordinate is in the component's ("this") coordinate system.
     * The interval begin time is included in the calculation of the time at
     * the given coordinate.
     *
     * @param x x-coordinate
     *
     * @return the mediatime corresponding to the specified position
     */
    public long timeAt(int x) {
        return intervalBeginTime + (x * msPerPixel);
    }

    /**
     * Calculates the x coordinate in virtual image space.<br>
     * This virtual image would be an image of width <br>
     * media duration in ms / ms per pixel. Therefore the return value does
     * not correct for interval begin time and is not necessarily within the
     * bounds of this component.
     *
     * @param theTime the media time
     *
     * @return the x coordinate in the virtual image space
     */
    private int timeToPixels(long theTime) {
        return (int) theTime / msPerPixel;
    }

    /**
     * Calculates the time corresponding to a pixel location in the virtual
     * image space.
     *
     * @param x the x coordinate in virtual image space
     *
     * @return the media time at the specified point
     */
    private long pixelToTime(int x) {
        return (long) (x * msPerPixel);
    }

    /**
     * Implements updateTimeScale from TimeScaleBasedViewer to adjust the
     * TimeScale if needed and when in TimeScale connected mode.<br>
     * Checks the GlobalTimeScaleIntervalBeginTime and
     * GlobalTimeScaleMsPerPixel and adjusts the interval and resolution of
     * this viewer when they differ from the global values.<br>
     * For the time being assume that the viewer is notified only once when
     * the resolution or the interval begintime has changed.
     */
    public void updateTimeScale() {
        if (timeScaleConnected) {
            //if the resolution is changed recalculate the begin time
            if (getGlobalTimeScaleMsPerPixel() != msPerPixel) {
                setLocalTimeScaleMsPerPixel(getGlobalTimeScaleMsPerPixel());
            } else if (getGlobalTimeScaleIntervalBeginTime() != intervalBeginTime) {
                //assume the resolution has not been changed
                setLocalTimeScaleIntervalBeginTime(getGlobalTimeScaleIntervalBeginTime());

                //System.out.println("update begin time in TimeLineViewer called");
            }
        }
    }

    /**
     * Sets whether or not this viewer listens to global time scale updates.
     *
     * @param connected the new timescale connected value
     */
    public void setTimeScaleConnected(boolean connected) {
        timeScaleConnected = connected;

        if (timeScaleConnected) {
            if (msPerPixel != getGlobalTimeScaleMsPerPixel()) {
                setLocalTimeScaleMsPerPixel(getGlobalTimeScaleMsPerPixel());
            }

            if (intervalBeginTime != getGlobalTimeScaleIntervalBeginTime()) {
                setLocalTimeScaleIntervalBeginTime(getGlobalTimeScaleIntervalBeginTime());
            }
        }
    }

    /**
     * Gets whether this viewer listens to time scale updates from other
     * viewers.
     *
     * @return true when connected to global time scale values, false otherwise
     */
    public boolean getTimeScaleConnected() {
        return timeScaleConnected;
    }

    /**
     * Checks whether this viewer is TimeScale connected and changes the
     * milliseconds per pixel value globally or locally.
     *
     * @param mspp the new milliseconds per pixel value
     */
    public void setMsPerPixel(int mspp) {
        if (timeScaleConnected) {
            setGlobalTimeScaleMsPerPixel(mspp);
            setGlobalTimeScaleIntervalBeginTime(intervalBeginTime);
            setGlobalTimeScaleIntervalEndTime(intervalEndTime);
        } else {
            setLocalTimeScaleMsPerPixel(mspp);
        }
    }

    /**
     * Change the horizontal resolution or zoomlevel locally. The msPerPixel
     * denotes the number of milliseconds of which the sound samples should be
     * merged to one value. It corresponds to one pixel in image space (a
     * pixel is the smallest unit in image space).<br>
     * The position on the screen of crosshair cursor should change as little
     * as possible.<br>
     * This is calculated as follows:<br>
     * The absolute x coordinate in image space is the current media time
     * divided by the new msPerPixel.<br>
     * <pre>
     * |----------|----------|-------x--|-- <br>
     * |imagesize |                  | absolute x coordinate of media time<br>
     * |    1     |    2     |    3     |
     * </pre>
     * Calculate the number of screen images that fit within the absolute x
     * coordinate. The new position on the screen would then be the absolute x
     * coordinate minus the number of screen images multiplied by the image
     * width. The difference between the old x value and the new x value is
     * then used to calculte the new interval start time.<br>
     * The new start time = (number of screen images  image width -
     * difference)  msPerPixel.
     *
     * @param step the new horizontal zoomlevel
     */
    private void setLocalTimeScaleMsPerPixel(int step) {
        if (msPerPixel == step) {
            return;
        }

        if (step >= 1) {
            msPerPixel = step;
        } else {
            msPerPixel = 1;
        }

        resolution = (int) (1000f / msPerPixel);

        /*stop the player if necessary*/
        boolean playing = playerIsPlaying();

        if (playing) {
            stopPlayer();
        }

        long mediaTime = getMediaTime();
        int oldScreenPos = crossHairPos;
        int newMediaX = (int) (mediaTime / msPerPixel);
        int numScreens;

        if (imageWidth > 0) {
            numScreens = (int) (mediaTime / (imageWidth * msPerPixel));
        } else {
            numScreens = 0;
        }

        int newScreenPos = newMediaX - (numScreens * imageWidth);
        int diff = oldScreenPos - newScreenPos;

        //new values
        intervalBeginTime = ((numScreens * imageWidth) - diff) * msPerPixel;

        if (intervalBeginTime < 0) {
            intervalBeginTime = 0;
        }

        intervalEndTime = intervalBeginTime + (imageWidth * msPerPixel);
        recalculateTagSizes();
        crossHairPos = xAt(mediaTime);
        selectionBeginPos = xAt(getSelectionBeginTime());
        selectionEndPos = xAt(getSelectionEndTime());
        paintBuffer();

        if (playing) {
            startPlayer();
        }

        int zoom = (int) (100f * (10f / msPerPixel));

        if (zoom <= 0) {
            zoom = 100;
        }

        updateZoomPopup(zoom);

        //repaint();
    }

    /**
     * Returns the current msPerPixel.
     *
     * @return msPerPixel
     */
    public int getMsPerPixel() {
        return msPerPixel;
    }

    /**
     * Calls #setMsPerPixel with the appropriate value. In setMsPerPixel the
     * value of this.resolution is actually set. msPerPixel = 1000 / resolution<br>
     * resolution = 1000 / msPerPixel
     *
     * @param resolution the new resolution
     */
    public void setResolution(int resolution) {
        if (resolution < 1) {
            this.resolution = 1;
        } else {
            this.resolution = resolution;
        }

        int mspp = (int) (1000f / resolution);
        setMsPerPixel(mspp);
    }

    /**
     * Sets the resolution by providing a factor the default PIXELS_FOR_SECOND
     * should be multiplied with.<br>
     * resolution = factor  PIXELS_FOR_SECOND.<br>
     * <b>Note:</b><br>
     * The factor = 100 / resolution_menu_percentage !
     *
     * @param factor the multiplication factor
     */
    public void setResolutionFactor(float factor) {
        int res = (int) (PIXELS_FOR_SECOND * factor);
        setResolution(res);
    }

    /**
     * Gets the current resolution
     *
     * @return the current resolution
     */
    public int getResolution() {
        return resolution;
    }

    /**
     * Find the tag at the given location.
     *
     * @param p the location
     * @param tierIndex the tier the tag should be found in
     *
     * @return the tag
     */
    private Tag2D getTagAt(Point2D p, int tierIndex) {
        if ((tierIndex < 0) || (tierIndex > (visibleTiers.size() - 1))) {
            return null;
        }

        long pTime = pixelToTime((int) p.getX());
        Tag2D t2d;
        Iterator it = ((Tier2D) visibleTiers.get(tierIndex)).getTags();

        while (it.hasNext()) {
            t2d = (Tag2D) it.next();

            if ((pTime >= t2d.getBeginTime()) && (pTime <= t2d.getEndTime())) {
                return t2d;
            }
        }

        return null;
    }

    /**
     * Calculate the index in the visible tiers array for the given y
     * coordinate.
     *
     * @param p DOCUMENT ME!
     *
     * @return the index of the Tier2D  or -1 when not found
     */
    private int getTierIndexForPoint(Point2D p) {
        int y = (int) p.getY() - rulerHeight;

        if ((y < 0) || (y > (visibleTiers.size() * pixelsForTierHeight))) {
            return -1;
        } else {
            return y / pixelsForTierHeight;
        }
    }

    /**
     * Calculate the index in the visible tiers array for the given annotation.
     *
     * @param annotation DOCUMENT ME!
     *
     * @return the index of the Tier2D or -1 when not found
     */
    private int getTierIndexForAnnotation(Annotation annotation) {
        Tier tier = annotation.getTier();
        int index = -1;

        for (int i = 0; i < visibleTiers.size(); i++) {
            if (((Tier2D) visibleTiers.get(i)).getTier() == tier) {
                index = i;

                break;
            }
        }

        return index;
    }

    /**
     * Inverts the point and finds the Tag2D at that point.
     *
     * @param p point in component space
     *
     * @return a tag2d or null
     */
    private Tag2D getHoverTag(Point p) {
        p.x += timeToPixels(intervalBeginTime);
        p.y += verticalScrollOffset;
        hoverTierIndex = getTierIndexForPoint(p);

        Tag2D hover = getTagAt(p, hoverTierIndex);

        return hover;
    }

    /**
     * Update the dragedit tag2d while dragging. <br>
     * Checks on the parent's boundaries (if any).
     *
     * @param dragEndPoint the position of the mouse pointer
     */
    private void updateDragEditTag(Point dragEndPoint) {
        if (dragEditTag2D == null) {
            return;
        }

        int diff = dragEndPoint.x - dragStartPoint.x;

        switch (dragEditMode) {
        case DRAG_EDIT_CENTER:

            if (dragParentBegin == -1) {
                dragEditTag2D.setX(dragEditTag2D.getX() + diff);
                dragStartPoint = dragEndPoint;
            } else {
                long bt = pixelToTime(dragEditTag2D.getX() + diff);

                if (diff < 0) {
                    if (bt < dragParentBegin) {
                        bt = dragParentBegin;

                        int nx = timeToPixels(bt);
                        dragEditTag2D.setX(nx);
                    } else {
                        dragEditTag2D.setX(dragEditTag2D.getX() + diff);
                        dragStartPoint = dragEndPoint;
                    }
                } else {
                    long et = pixelToTime(dragEditTag2D.getX() +
                            dragEditTag2D.getWidth() + diff);

                    if (et > dragParentEnd) {
                        et = dragParentEnd;
                        bt = et - pixelToTime(dragEditTag2D.getWidth());

                        dragEditTag2D.setX(timeToPixels(bt));
                    } else {
                        dragEditTag2D.setX(dragEditTag2D.getX() + diff);
                        dragStartPoint = dragEndPoint;
                    }
                }
            }

            setMediaTime(pixelToTime(dragEditTag2D.getX()));

            break;

        case DRAG_EDIT_LEFT:

            if ((dragEditTag2D.getX() + diff) < ((dragEditTag2D.getX() +
                    dragEditTag2D.getWidth()) - 1)) {
                if ((dragParentBegin == -1) || (diff > 0)) {
                    dragEditTag2D.setX(dragEditTag2D.getX() + diff);
                    dragEditTag2D.setWidth(dragEditTag2D.getWidth() - diff);
                    dragStartPoint = dragEndPoint;
                } else if ((dragParentBegin > -1) && (diff < 0)) {
                    long bt = pixelToTime(dragEditTag2D.getX() + diff);

                    if (bt < dragParentBegin) {
                        bt = dragParentBegin;

                        int nx = timeToPixels(bt);
                        dragEditTag2D.setX(nx);
                        dragEditTag2D.setWidth(timeToPixels(dragEditTag2D.getEndTime() -
                                bt));
                    } else {
                        dragEditTag2D.setX(dragEditTag2D.getX() + diff);
                        dragEditTag2D.setWidth(dragEditTag2D.getWidth() - diff);
                        dragStartPoint = dragEndPoint;
                    }
                }

                setMediaTime(pixelToTime(dragEditTag2D.getX()));
            }

            break;

        case DRAG_EDIT_RIGHT:

            if ((dragEditTag2D.getWidth() + diff) > 1) {
                if ((dragParentEnd == -1) || (diff < 0)) {
                    dragEditTag2D.setWidth(dragEditTag2D.getWidth() + diff);
                    dragStartPoint = dragEndPoint;
                } else if ((dragParentEnd > -1) && (diff > 0)) {
                    long et = pixelToTime(dragEditTag2D.getX() +
                            dragEditTag2D.getWidth() + diff);

                    if (et > dragParentEnd) {
                        et = dragParentEnd;
                        dragEditTag2D.setWidth(timeToPixels(et) -
                            dragEditTag2D.getX());
                    } else {
                        dragEditTag2D.setWidth(dragEditTag2D.getWidth() + diff);
                        dragStartPoint = dragEndPoint;
                    }
                }

                setMediaTime(pixelToTime(dragEditTag2D.getX() +
                        dragEditTag2D.getWidth()));
            }

            break;
        }

        repaint();
    }

    /**
     * DOCUMENT ME!
     *
     * @return the current interval begin time
     */
    public long getIntervalBeginTime() {
        return intervalBeginTime;
    }

    /**
     * DOCUMENT ME!
     *
     * @return the current interval end time
     */
    public long getIntervalEndTime() {
        return intervalEndTime;
    }

    /**
     * Checks whether this viewer is TimeScale connected and changes the
     * interval begin time globally or locally.
     *
     * @param begin the new interval begin time
     */
    public void setIntervalBeginTime(long begin) {
        if (timeScaleConnected) {
            setGlobalTimeScaleIntervalBeginTime(begin);
            setGlobalTimeScaleIntervalEndTime(intervalEndTime);
        } else {
            setLocalTimeScaleIntervalBeginTime(begin);
        }
    }

    /**
     * Calculates the new interval begin and/or end time.<br>
     * There are two special cases taken into account:<br>
     *
     * <ul>
     * <li>
     * when the player is playing attempts are made to shift the interval
     * <i>n</i> times the interval size to the left or to the right, until the
     * new interval contains the new mediatime.
     * </li>
     * <li>
     * when the player is not playing and the new interval begin time coincides
     * with the selection begin time, the interval is shifted a certain offset
     * away from the image edge. Same thing when the interval end time
     * coincides with the selection end time.
     * </li>
     * </ul>
     *
     *
     * @param mediaTime
     */
    private void recalculateInterval(final long mediaTime) {
        long newBeginTime = intervalBeginTime;
        long newEndTime = intervalEndTime;

        if (playerIsPlaying()) {
            // we might be in a selection outside the new interval
            // shift the interval n * intervalsize to the left or right
            if (mediaTime > intervalEndTime) {
                newBeginTime = intervalEndTime;
                newEndTime = newBeginTime + (imageWidth * msPerPixel);

                while ((newEndTime += (imageWidth + msPerPixel)) < mediaTime) {
                    newBeginTime += (imageWidth * msPerPixel);
                }
            } else if (mediaTime < intervalBeginTime) {
                newEndTime = intervalBeginTime;
                newBeginTime = newEndTime - (imageWidth * msPerPixel);

                while ((newEndTime -= (imageWidth * msPerPixel)) > mediaTime) {
                    newBeginTime -= (imageWidth * msPerPixel);
                }

                if (newBeginTime < 0) {
                    newBeginTime = 0;
                    newEndTime = imageWidth * msPerPixel;
                }
            } else {
                // the new time appears to be in the current interval after all
                return;
            }
        } else { //player is not playing

            // is the new media time to the left or to the right of the current interval
            if (mediaTime < intervalBeginTime) {
                newBeginTime = mediaTime - (SCROLL_OFFSET * msPerPixel);

                if (newBeginTime < 0) {
                    newBeginTime = 0;
                }

                newEndTime = newBeginTime + (imageWidth * msPerPixel);
            } else if (mediaTime > intervalEndTime) {
                newEndTime = mediaTime + (SCROLL_OFFSET * msPerPixel);
                newBeginTime = newEndTime - (imageWidth * msPerPixel);

                if (newBeginTime < 0) { // somehing would be wrong??
                    newBeginTime = 0;
                    newEndTime = newBeginTime + (imageWidth * msPerPixel);
                }
            }

            if ((newBeginTime == getSelectionBeginTime()) &&
                    (newBeginTime > (SCROLL_OFFSET * msPerPixel))) {
                newBeginTime -= (SCROLL_OFFSET * msPerPixel);
                newEndTime = newBeginTime + (imageWidth * msPerPixel);
            }

            if (newEndTime == getSelectionEndTime()) {
                newEndTime += (SCROLL_OFFSET * msPerPixel);
                newBeginTime = newEndTime - (imageWidth * msPerPixel);

                if (newBeginTime < 0) { // something would be wrong??
                    newBeginTime = 0;
                    newEndTime = newBeginTime + (imageWidth * msPerPixel);
                }
            }

            // try to position the whole selection in the view
            if ((mediaTime == getSelectionBeginTime()) &&
                    (getSelectionEndTime() > (newEndTime -
                    (SCROLL_OFFSET * msPerPixel))) && !panMode) {
                newEndTime = getSelectionEndTime() +
                    (SCROLL_OFFSET * msPerPixel);
                newBeginTime = newEndTime - (imageWidth * msPerPixel);

                if ((newBeginTime > mediaTime) &&
                        (mediaTime > (SCROLL_OFFSET * msPerPixel))) {
                    newBeginTime = mediaTime - (SCROLL_OFFSET * msPerPixel);
                    newEndTime = newBeginTime + (imageWidth * msPerPixel);
                } else if (newBeginTime > mediaTime) {
                    newBeginTime = 0;
                    newEndTime = imageWidth * msPerPixel;
                }
            }
        }

        if (timeScaleConnected) {
            //System.out.println("TLV new begin time: " + newBeginTime);
            //System.out.println("TLV new end time: " + newEndTime);
            setGlobalTimeScaleIntervalBeginTime(newBeginTime);
            setGlobalTimeScaleIntervalEndTime(newEndTime);
        } else {
            setLocalTimeScaleIntervalBeginTime(newBeginTime);
        }
    }

    /**
     * Changes the interval begin time locally.
     *
     * @param begin the new local interval begin time
     */
    private void setLocalTimeScaleIntervalBeginTime(long begin) {
        if (begin == intervalBeginTime) {
            return;
        }

        intervalBeginTime = begin;
        intervalEndTime = intervalBeginTime + (imageWidth * msPerPixel);

        //
        if (editBox.isVisible()) {
            if (getActiveAnnotation() != null) {
                int x = xAt(getActiveAnnotation().getBeginTimeBoundary());
                editBox.setLocation(x, editBox.getY());
            } else {
                dismissEditBox();
            }

            /*
               if (x < 0 || x > imageWidth) {
                   dismissEditBox();
               } else {
                   editBox.setLocation(x, editBox.getY());
               }
             */
        }

        //
        crossHairPos = xAt(crossHairTime);
        selectionBeginPos = xAt(getSelectionBeginTime());
        selectionEndPos = xAt(getSelectionEndTime());
        paintBuffer();
    }

    /**
     * DOCUMENT ME!
     *
     * @return the vertical scroll offset
     */
    public int getVerticalScrollOffset() {
        return verticalScrollOffset;
    }

    /**
     * Sets the vertical scroll offset of the tags on this component. <b>Note:</b><br>
     * There should be some kind of synchronization with other viewers:
     * pending..
     *
     * @param offset the new vertical scroll offset
     */
    public void setVerticalScrollOffset(int offset) {
        verticalScrollOffset = offset;
        repaint();
    }

    /**
     * Scrolls the viewport vertically to ensure the cursorTag is visible.
     */
    private void ensureVerticalVisibilityOfActiveAnnotation() {
        if (cursorTag2D == null) {
            return;
        }

        int cy = cursorTierIndex * pixelsForTierHeight;

        if (cy < verticalScrollOffset) {
            scrollBar.setValue(cy);
        } else if (((cy + pixelsForTierHeight) - verticalScrollOffset) > (getHeight() -
                rulerHeight)) {
            scrollBar.setValue((cy + pixelsForTierHeight + rulerHeight) -
                getHeight());
        }
    }

    /**
     * Update the values of the scrollbar.<br>
     * Called after a change in the number of visible tiers.
     */
    private void updateScrollBar() {
        int value = scrollBar.getValue();
        int max = (visibleTiers.size() * pixelsForTierHeight) + rulerHeight;

        // before changing scrollbar values do a setValue(0), otherwise
        // setMaxiimum and/or setVisibleAmount will not be accurate
        scrollBar.setValue(0);
        scrollBar.setMaximum(max);
        scrollBar.setVisibleAmount(getHeight());

        if ((value + getHeight()) > max) {
            value = max - getHeight();
        }

        scrollBar.setValue(value);
        scrollBar.revalidate();
    }

    /**
     * Calculate the y positions of the vertical middle of all visible tiers
     * and pass them to the MultiViewerController.
     */
    private void notifyMultiTierControlPanel() {
        if (multiTierControlPanel == null) {
            return;
        }

        //tierYPositions = new int[visibleTiers.size()];
        if (tierYPositions.length > 0) {
            tierYPositions[0] = (rulerHeight + (pixelsForTierHeight / 2)) -
                verticalScrollOffset;

            for (int i = 1; i < visibleTiers.size(); i++) {
                tierYPositions[i] = tierYPositions[0] +
                    (i * pixelsForTierHeight);
            }
        }

        multiTierControlPanel.setTierPositions(tierYPositions);
    }

    /**
     * Returns the actual size of the viewer (viewable area), i.e. the size of
     * the component minus the size of the scrollbar.<br>
     * Needed for the accurate alignment with other viewers.
     *
     * @return the actual size of the viewer
     */
    public Dimension getViewerSize() {
        return new Dimension(imageWidth, imageHeight);
    }

    /**
     * Layout information, gives the nr of pixels at the left of the viewer
     * panel that contains no time line information
     *
     * @return the nr of pixels at the left that contain no time line related
     *         data
     */
    public int getLeftMargin() {
        return 0;
    }

    /**
     * Layout information, gives the nr of pixels at the right of the viewer
     * panel that contains no time line information
     *
     * @return the nr of pixels at the right that contain no time line related
     *         data
     */
    public int getRightMargin() {
        return scrollBar.getWidth();
    }

    /**
     * Create a popup menu to enable the manipulation of some settings for this
     * viewer.
     */
    private void createPopupMenu() {
        popup = new JPopupMenu("TimeLine Viewer");
        zoomMI = new JMenu(ElanLocale.getString("TimeScaleBasedViewer.Zoom"));
        zoomBG = new ButtonGroup();

        //
        JRadioButtonMenuItem zoomRB;

        for (int i = 0; i < ZOOMLEVELS.length; i++) {
            zoomRB = new JRadioButtonMenuItem(ZOOMLEVELS[i] + "%");
            zoomRB.setActionCommand(String.valueOf(ZOOMLEVELS[i]));
            zoomRB.addActionListener(this);
            zoomBG.add(zoomRB);
            zoomMI.add(zoomRB);

            if (ZOOMLEVELS[i] == 100) {
                zoomRB.setSelected(true);
            }
        }

        popup.add(zoomMI);

        // font size items		
        int fontSize = getFont().getSize();
        fontSizeBG = new ButtonGroup();
        fontMenu = new JMenu(ElanLocale.getString("Menu.View.FontSize"));

        JRadioButtonMenuItem fontRB;

        for (int i = 0; i < Constants.FONT_SIZES.length; i++) {
            fontRB = new JRadioButtonMenuItem(String.valueOf(
                        Constants.FONT_SIZES[i]));
            fontRB.setActionCommand("font" + Constants.FONT_SIZES[i]);

            if (fontSize == Constants.FONT_SIZES[i]) {
                fontRB.setSelected(true);
            }

            fontRB.addActionListener(this);
            fontSizeBG.add(fontRB);
            fontMenu.add(fontRB);
        }

        popup.add(fontMenu);

        activeAnnStrokeBoldMI = new JCheckBoxMenuItem(ElanLocale.getString(
                    "TimeLineViewer.Menu.Stroke"));
        activeAnnStrokeBoldMI.setActionCommand("aastroke");
        activeAnnStrokeBoldMI.addActionListener(this);
        popup.add(activeAnnStrokeBoldMI);

        popup.addSeparator();

        timeScaleConMI = new JCheckBoxMenuItem(ElanLocale.getString(
                    "TimeScaleBasedViewer.Connected"), timeScaleConnected);
        timeScaleConMI.setActionCommand("connect");
        timeScaleConMI.addActionListener(this);
        popup.add(timeScaleConMI);

        popup.addSeparator();

        // tier menu items
        activeTierMI = new JMenuItem(ElanLocale.getString(
                    "Menu.Edit.ActiveTier"));
        activeTierMI.setActionCommand("activeTier");
        activeTierMI.addActionListener(this);
        popup.add(activeTierMI);

        deleteTierMI = new JMenuItem(ElanLocale.getString(
                    "Menu.Edit.DeleteTier"));
        deleteTierMI.setActionCommand("deleteTier");
        deleteTierMI.addActionListener(this);
        popup.add(deleteTierMI);

        changeTierMI = new JMenuItem(ElanLocale.getString(
                    "Menu.Edit.ChangeTierAttributes"));

        //changeTierMI = new JMenuItem(ELANCommandFactory.CHANGE_TIER_ATTR);
        changeTierMI.setActionCommand("changeTier");
        changeTierMI.addActionListener(this);
        popup.add(changeTierMI);

        popup.addSeparator();

        // annotation menu items
        newAnnoMI = new JMenuItem(ElanLocale.getString(
                    "Menu.Edit.NewAnnotation"));
        newAnnoMI.setActionCommand("newAnn");
        newAnnoMI.addActionListener(this);
        popup.add(newAnnoMI);

        newAnnoBeforeMI = new JMenuItem(ElanLocale.getString(
                    "Menu.Edit.NewAnnotationBefore"));
        newAnnoBeforeMI.setActionCommand("annBefore");
        newAnnoBeforeMI.addActionListener(this);
        popup.add(newAnnoBeforeMI);

        newAnnoAfterMI = new JMenuItem(ElanLocale.getString(
                    "Menu.Edit.NewAnnotationAfter"));
        newAnnoAfterMI.setActionCommand("annAfter");
        newAnnoAfterMI.addActionListener(this);
        popup.add(newAnnoAfterMI);

        modifyAnnoMI = new JMenuItem(ElanLocale.getString(
                    "Menu.Edit.ModifyAnnotation"));
        modifyAnnoMI.setActionCommand("modifyAnn");
        modifyAnnoMI.addActionListener(this);
        popup.add(modifyAnnoMI);

        modifyAnnoTimeMI = new JMenuItem(ELANCommandFactory.getCommandAction(
                    transcription, ELANCommandFactory.MODIFY_ANNOTATION_TIME));
        popup.add(modifyAnnoTimeMI);

        deleteAnnoMI = new JMenuItem(ElanLocale.getString(
                    "Menu.Edit.DeleteAnnotation"));
        deleteAnnoMI.setActionCommand("deleteAnn");
        deleteAnnoMI.addActionListener(this);
        popup.add(deleteAnnoMI);

        JPopupMenu.setDefaultLightWeightPopupEnabled(false);

        int zoom = (int) (100f * (10f / msPerPixel));

        if (zoom <= 0) {
            zoom = 100;
        }

        updateZoomPopup(zoom);
    }

    /**
     * Updates the "zoom" menu item. Needed, when timeScaleConnected, after a
     * change of the zoomlevel in some other connected viewer.
     *
     * @param zoom the zoom level
     */
    private void updateZoomPopup(int zoom) {
        // first find the closest match (ther can be rounding issues)
        int zoomMenuIndex = 0;
        int diff = Integer.MAX_VALUE;

        for (int i = 0; i < ZOOMLEVELS.length; i++) {
            int d = Math.abs(ZOOMLEVELS[i] - zoom);

            if (d < diff) {
                diff = d;
                zoomMenuIndex = i;
            }
        }

        if (popup != null) {
            java.util.Enumeration en = zoomBG.getElements();
            int counter = 0;

            while (en.hasMoreElements()) {
                JRadioButtonMenuItem rbmi = (JRadioButtonMenuItem) en.nextElement();

                if (counter == zoomMenuIndex) { //rbmi.getActionCommand().equals(zoomLevel)
                    rbmi.setSelected(true);
                } else {
                    rbmi.setSelected(false);
                }

                counter++;
            }
        }
    }

    /**
     * Enables / disables annotation and tier specific menuitems, depending on
     * the mouse click position.<br>
     * <b>Note: </b> this might need to be changed once usage of Action and
     * Command objects is implemented.
     *
     * @param p the position of the mouse click
     */
    private void updatePopup(Point p) {
        //disable all first
        newAnnoMI.setEnabled(false);
        newAnnoBeforeMI.setEnabled(false);
        newAnnoAfterMI.setEnabled(false);
        modifyAnnoMI.setEnabled(false);
        modifyAnnoTimeMI.setEnabled(false);
        deleteAnnoMI.setEnabled(false);
        activeTierMI.setEnabled(false);
        deleteTierMI.setEnabled(false);
        changeTierMI.setEnabled(false);

        if (rulerAlwaysVisible && (p.y < rulerHeight)) {
            return;
        } else {
            Point inverse = new Point(p);

            //compensate for the intervalBeginTime
            inverse.x += timeToPixels(intervalBeginTime);
            inverse.y += verticalScrollOffset;
            rightClickTime = pixelToTime(inverse.x);

            if (rightClickTime > getMediaDuration()) {
                return;
            }

            boolean supportsInsertion = false;

            TierImpl tier;

            //DobesTag tag;
            Tag2D tag2d;
            int tierIndex = getTierIndexForPoint(inverse);

            if (tierIndex < 0) {
                return;
            }

            rightClickTier = (Tier2D) visibleTiers.get(tierIndex);
            tier = rightClickTier.getTier();

            if (tier == null) {
                return;
            }

            try {
                LinguisticType lt = tier.getLinguisticType();
                Constraint c = null;

                if (lt != null) {
                    c = lt.getConstraints();
                }

                if (c != null) {
                    supportsInsertion = c.supportsInsertion();
                }

                deleteTierMI.setEnabled(true);
                changeTierMI.setEnabled(true);

                if (!rightClickTier.isActive()) {
                    activeTierMI.setEnabled(true);
                }

                tag2d = getTagAt(inverse, tierIndex);

                if ((tag2d != null) && (tag2d == cursorTag2D)) {
                    modifyAnnoMI.setEnabled(true);
                    deleteAnnoMI.setEnabled(true);

                    if (supportsInsertion) {
                        newAnnoAfterMI.setEnabled(true);
                        newAnnoBeforeMI.setEnabled(true);
                    }

                    // removed else here...
                    if (tier.isTimeAlignable()) {
                        newAnnoMI.setEnabled(true); //replace an existing annotation??

                        if (getSelectionBeginTime() != getSelectionEndTime()) {
                            modifyAnnoTimeMI.setEnabled(true);
                        }
                    }
                } else {
                    // check this, much to complicated
                    // this should be based on the Constraints object..
                    if ((getSelectionBeginTime() != getSelectionEndTime()) &&
                            (getSelectionBeginTime() <= rightClickTime) &&
                            (getSelectionEndTime() >= rightClickTime)) {
                        if (tier.isTimeAlignable()) {
                            newAnnoMI.setEnabled(true);
                        } else {
                            if ((tier.getParentTier() != null) &&
                                    ((c.getStereoType() == Constraint.SYMBOLIC_ASSOCIATION) ||
                                    (c.getStereoType() == Constraint.SYMBOLIC_SUBDIVISION))) {
                                Annotation parentA = ((TierImpl) tier.getParentTier()).getAnnotationAtTime(rightClickTime);
                                Annotation refA = tier.getAnnotationAtTime(rightClickTime);

                                if ((parentA != null) && (refA == null)) {
                                    newAnnoMI.setEnabled(true);
                                }
                            }
                        }
                    }
                }
            } catch (Exception rex) {
                rex.printStackTrace();
            }
        }
    }

    /**
     * Set the inline edit box to invisible by canceling the edit.
     */
    private void dismissEditBox() {
        if (editBox.isVisible()) {
            editBox.cancelEdit();
        }
    }

    /**
     * Display the edit box for the specified Tag2D
     *
     * @param tag2d the tag to edit
     */
    protected void showEditBoxForTag(Tag2D tag2d) {
        if (tag2d.getAnnotation() == null) {
            return;
        }

        editBox.setAnnotation(tag2d.getAnnotation(),
            forceOpenControlledVocabulary);

        int tierIndex = getTierIndexForAnnotation(tag2d.getAnnotation());
        int y = (rulerHeight + (tierIndex * pixelsForTierHeight)) -
            verticalScrollOffset;
        int x = xAt(tag2d.getBeginTime());

        /* //scroll to the beginning of the tag
           if (x < 0) {
               x = 0;
               setIntervalBeginTime(tag2d.getBeginTime());
           }
         */
        editBox.setLocation(x, y);
        editBox.configureEditor(JPanel.class, null,
            new Dimension(tag2d.getWidth() + 2, pixelsForTierHeight));
        editBox.startEdit();

        forceOpenControlledVocabulary = false;
    }

    private void showEditBoxForAnnotation(Annotation ann) {
        if (ann == null) {
            return;
        }

        editBox.setAnnotation(ann);

        int tierIndex = getTierIndexForAnnotation(ann);
        int y = (rulerHeight + (tierIndex * pixelsForTierHeight)) -
            verticalScrollOffset;
        int x = xAt(ann.getBeginTimeBoundary());
        int w = xAt(ann.getEndTimeBoundary()) - x;

        /* //scroll to the beginning of the tag
           if (x < 0) {
               x = 0;
               setIntervalBeginTime(ann.getBeginTimeBoundary());
           }
         */
        editBox.setLocation(x, y);
        editBox.configureEditor(JPanel.class, null,
            new Dimension(w + 2, pixelsForTierHeight));
        editBox.startEdit();
    }

    // ***** editing and data changed methods **************************************//

    /**
     * Change a single annotation.
     *
     * @param tier the tier the annotation is part of
     * @param ann the annotation that has been changed
     */
    private void annotationChanged(TierImpl tier, Annotation ann) {
        Iterator allTierIt = allTiers.iterator();
        dismissEditBox();
alltierloop: 
        while (allTierIt.hasNext()) {
            Tier2D t2d = (Tier2D) allTierIt.next();

            if (t2d.getTier() == tier) {
                ArrayList tagList = t2d.getTagsList();
alltagloop: 
                for (int i = 0; i < tagList.size(); i++) {
                    Tag2D tag2d = (Tag2D) tagList.get(i);

                    // check equality with ==
                    if (tag2d.getAnnotation() == ann) {
                        tag2d.setTruncatedValue(truncateString(ann.getValue(),
                                tag2d.getWidth(), metrics));

                        break alltierloop;
                    }
                }
            }
        }

        paintBuffer();
    }

    /**
     * Add a new Tier to the existing list of tiers.<br>
     * This method is private because it does not check whether the specified
     * Tier already is present in the transcription.
     *
     * @param tier the new Tier
     */
    private void tierAdded(TierImpl tier) {
        Tag2D tag2d;
        int xPos;
        int tagWidth;

        Tier2D tier2d = new Tier2D(tier);
        Iterator annotIter = tier.getAnnotations().iterator();

        while (annotIter.hasNext()) {
            Annotation a = (Annotation) annotIter.next();

            //System.out.println("Annotation: " + a);
            tag2d = new Tag2D(a);
            xPos = timeToPixels(a.getBeginTimeBoundary());
            tag2d.setX(xPos);
            tagWidth = timeToPixels(a.getEndTimeBoundary()) - xPos;
            tag2d.setWidth(tagWidth);
            tag2d.setTruncatedValue(truncateString(a.getValue(), tagWidth,
                    metrics));
            tier2d.addTag(tag2d);
        }

        allTiers.add(tier2d);
        tierYPositions = new int[allTiers.size()];

        //wait for a call to setVisibleTiers to show the tier
        //visibleTiers.add(tier2d);
        //paintBuffer();
        //System.out.println("new tier: " + tier2d.getName());
    }

    /**
     * Remove a Tier from the list of tiers.
     *
     * @param tier the Tier to remove
     */
    private void tierRemoved(TierImpl tier) {
        dismissEditBox();

        for (int i = 0; i < allTiers.size(); i++) {
            Tier2D tier2d = (Tier2D) allTiers.get(i);

            if (tier2d.getTier() == tier) {
                allTiers.remove(i);

                //wait for a call to setVisibleTiers
                if ((cursorTag2D != null) &&
                        (cursorTag2D.getTier2D() == tier2d)) {
                    cursorTag2D = null;
                    setActiveAnnotation(null);
                }

                break;
            }
        }

        //repaint();
    }

    /**
     * Check the name of the Tier2D object of the specified Tier.<br>
     * Other changes to the tier such as linguistic type or parent are
     * expected to become manifest through (a series of) changes in
     * annotations.
     *
     * @param tier the Tier that has been changed
     */
    private void tierChanged(TierImpl tier) {
        Vector depTiers = new Vector();

        if (tier != null) {
            depTiers = tier.getDependentTiers();
        }

        for (int i = 0; i < allTiers.size(); i++) {
            Tier2D tier2d = (Tier2D) allTiers.get(i);

            if (tier2d.getTier() == tier) {
                tier2d.updateName();
            }

            if ((tier2d.getTier() == tier) ||
                    depTiers.contains(tier2d.getTier())) {
                reextractTagsForTier(tier2d);
            }
        }
    }

    /**
     * Create a Tag2D for a new Annotation on the Tier2D of the specified TierImpl.<br>
     * If the Transcription is in Bulldozer mode, reextract the Tier2D.
     * Correction: transcription does not have a bulldozer mode (yet), just
     * reextract the tier...
     *
     * @param tiers the Tier the annotation belongs to
     */

    /*
       private void annotationAdded(TierImpl tier, Annotation annotation) {
           dismissEditBox();

               for (int i = 0; i < allTiers.size(); i++) {
                   Tier2D tier2d = (Tier2D) allTiers.get(i);

                   if (tier2d.getTier() == tier) {
                       reextractTagsForTier(tier2d);

                       paintBuffer();

                       break;
                   }
               }
           }
     */

    /**
     * Called when an annotation has been added before or after another
     * annotation, effecting more than one or two annotations.
     *
     * @param tiers a Vector of tiers that have been changed
     */
    private void annotationsAdded(Vector tiers) {
        int mode = transcription.getTimeChangePropagationMode();

        if (mode != Transcription.SHIFT) {
            Tier2D tier2d;
            dismissEditBox();

            for (int i = 0; i < allTiers.size(); i++) {
                tier2d = (Tier2D) allTiers.get(i);

                if (tiers.contains(tier2d.getTier())) {
                    reextractTagsForTier(tier2d);
                }
            }

            paintBuffer();
        } else {
            transcriptionChanged();
        }
    }

    /**
     * This method inserts a new annotation if there is an empty slot at the
     * specified point on the tier for the specified index.
     *
     * @param p the location of a doubleclick
     * @param tierIndex tier index
     */
    private void autoInsertAnnotation(Point p, int tierIndex) {
        if ((tierIndex < 0) || (tierIndex > (visibleTiers.size() - 1))) {
            return;
        }

        TierImpl child = ((Tier2D) visibleTiers.get(tierIndex)).getTier();

        if (child == null) {
            return;
        }

        long clickTime = pixelToTime((int) p.getX());

        if (child.isTimeAlignable() || !child.hasParentTier()) {
            if ((clickTime >= getSelectionBeginTime()) &&
                    (clickTime <= getSelectionEndTime())) {
                Command c = ELANCommandFactory.createCommand(transcription,
                        ELANCommandFactory.NEW_ANNOTATION);
                Object[] args = new Object[] {
                        getViewerManager(), new Long(getSelectionBeginTime()),
                        new Long(getSelectionEndTime())
                    };
                c.execute(child, args);
            }
        } else {
            TierImpl parent = (TierImpl) child.getParentTier();

            Annotation ann = parent.getAnnotationAtTime(clickTime);

            if (ann != null) {
                Command c = ELANCommandFactory.createCommand(transcription,
                        ELANCommandFactory.NEW_ANNOTATION);
                Object[] args = new Object[] {
                        getViewerManager(), new Long(clickTime),
                        new Long(clickTime)
                    };
                c.execute(child, args);
            }
        }
    }

    /**
     * Remove the Tag2D from the Tier2D corresponding to the respective
     * Annotation and TierImpl.
     */

    /*
       private void annotationRemoved(TierImpl tier, Annotation annotation) {
           Tier2D tier2d;
           Tag2D tag2d;

               for (int i = 0; i < allTiers.size(); i++) {
                   tier2d = (Tier2D) allTiers.get(i);

                   if (tier2d.getTier() == tier) {
                       Iterator tagIt = tier2d.getTags();

                       while (tagIt.hasNext()) {
                           tag2d = (Tag2D) tagIt.next();

                           if (tag2d.getAnnotation() == annotation) {
                               dismissEditBox();
                               tier2d.removeTag(tag2d);

                               return;
                           }
                       }
                   }
               }
           }
     */

    /**
     * This is called when an ACMEditEvent is received with operation
     * REMOVE_ANNOTATION and the transcription as invalidated object.<br>
     * It is undefined which tiers and annotations have been effected, so the
     * transcription is simply re-processed. Store state as much as possible.
     * Assume no tiers have been deleted or added.
     */
    private void annotationsRemoved() {
        transcriptionChanged();
    }

    /**
     * Called when begin and/or end time of an alignable annotation  has been
     * changed. In shift time propagation mode all tiers  are reextracted, in
     * other modes only the tiers that can be effected  are reextracted.
     *
     * @param tiers the vector of tiers that could be effected by the change
     */
    private void annotationTimeChanged(Vector tiers) {
        int mode = transcription.getTimeChangePropagationMode();

        if (mode != Transcription.SHIFT) {
            Tier2D tier2d;

            for (int i = 0; i < allTiers.size(); i++) {
                tier2d = (Tier2D) allTiers.get(i);

                if (tiers.contains(tier2d.getTier())) {
                    reextractTagsForTier(tier2d);
                }
            }

            paintBuffer();
        } else {
            transcriptionChanged();
        }
    }

    /**
     * This is called when an ACMEditEvent is received with an operation that
     * could influence all tiers in the transcription or with the
     * transcription as invalidated object.<br>
     * Examples are annotations_removed or annotation_added and
     * annotation_time_changed  in shift mode. The transcription is simply
     * re-processed. Store state as much as possible. Assume no tiers have
     * been deleted or added.
     */
    private void transcriptionChanged() {
        cursorTag2D = null;

        Tier2D tier2d;
        dismissEditBox();

        for (int i = 0; i < allTiers.size(); i++) {
            tier2d = (Tier2D) allTiers.get(i);
            reextractTagsForTier(tier2d);
        }

        if (cursorTag2D != null) {
            cursorTierIndex = visibleTiers.indexOf(cursorTag2D.getTier2D());
        }

        paintBuffer();
    }

    /**
     * Tries to retrieve the default Font size for tooltips.<br>
     * This can then be used when the changing the Font the tooltip has to
     * use.
     *
     * @return the default font size or 12 when not found
     */
    private int getDefaultTooltipFontSize() {
        Object value = UIManager.getDefaults().get("ToolTip.font");

        if ((value != null) && value instanceof Font) {
            return ((Font) value).getSize();
        }

        return 12;
    }

    /**
     * Override create tooltip to be able to set the (Unicode) Font for the
     * tip.
     *
     * @return DOCUMENT ME!
     */
    public JToolTip createToolTip() {
        JToolTip tip = new JToolTip();
        tip.setFont(tooltipFont);

        //tip.setFont(tooltipFont != null ? tooltipFont : this.getFont().deriveFont((float)tooltipFontSize));
        tip.setComponent(this);

        return tip;
    }

    /**
     * Set a new Transcription for this viewer.<br>
     * We should receive a setVisibleTiers() and a setActiveTier() call after
     * this but are faking it now.
     *
     * @param transcription the new transcription.
     */
    public void setTranscription(Transcription transcription) {
        this.transcription = transcription;
        hoverTag2D = null;
        hoverTierIndex = 0;
        cursorTag2D = null;
        cursorTierIndex = 0;

        //
        Vector oldVisibles = new Vector(visibleTiers.size());

        for (int i = 0; i < visibleTiers.size(); i++) {
            Tier2D tier2d = (Tier2D) visibleTiers.get(i);
            oldVisibles.add(tier2d.getTier());
        }

        String activeTierName = "";

        for (int i = 0; i < allTiers.size(); i++) {
            Tier2D tier2d = (Tier2D) allTiers.get(i);

            if (tier2d.isActive()) {
                activeTierName = tier2d.getName();

                break;
            }
        }

        //
        initTiers();

        //
        for (int i = 0; i < allTiers.size(); i++) {
            Tier2D tier2d = (Tier2D) allTiers.get(i);

            if (tier2d.getName().equals(activeTierName)) {
                tier2d.setActive(true);

                break;
            }
        }

        setVisibleTiers(oldVisibles);
    }

    //***** end of initial editing and data changed methods **************************************//
    //**************************************************************************************//

    /* implement ControllerListener */
    /* (non-Javadoc)
     * @see mpi.eudico.client.annotator.ControllerListener#controllerUpdate(mpi.eudico.client.annotator.ControllerEvent)
     */
    public void controllerUpdate(ControllerEvent event) {
        if (event instanceof TimeEvent || event instanceof StopEvent) {
            crossHairTime = getMediaTime();

            /*
               //System.out.println("TimeLineViewer time: " + crossHairTime);
               if (crossHairTime < intervalBeginTime || crossHairTime > intervalEndTime) {
                   if (playerIsPlaying()) {
                       // we might be in a selection outside the new interval
                       long newBeginTime = intervalEndTime;
                       long newEndTime = newBeginTime + (imageWidth * msPerPixel);
                       if (crossHairTime > newEndTime) {
                           while ((newEndTime += imageWidth + msPerPixel) < crossHairTime) {
                               newBeginTime += imageWidth * msPerPixel;
                           }
                       } else if (crossHairTime < newBeginTime) {
                           while ((newEndTime -= imageWidth * msPerPixel) > crossHairTime) {
                               newBeginTime -= imageWidth * msPerPixel;
                           }
                       }
                       setIntervalBeginTime(newBeginTime);
                   } else {
                       setIntervalBeginTime(crossHairTime);
                   }
                   crossHairPos = xAt(crossHairTime);
             */
            /*// replaced 07 apr 2005 better positioning of search result annotations
               if ((crossHairTime == intervalEndTime) && !playerIsPlaying()) {
                   recalculateInterval(crossHairTime);
               } else if ((crossHairTime < intervalBeginTime) ||
                       (crossHairTime > intervalEndTime)) {
                   //dismissEditBox();
                   recalculateInterval(crossHairTime);
               } */
            if (!playerIsPlaying()) {
                if (scroller == null) {
                    recalculateInterval(crossHairTime);
                    crossHairPos = xAt(crossHairTime);
                    repaint();
                } else {
                    recalculateInterval(crossHairTime);
                }
            } else {
                if ((crossHairTime < intervalBeginTime) ||
                        (crossHairTime > intervalEndTime)) {
                    //dismissEditBox();
                    recalculateInterval(crossHairTime);
                } else {
                    // repaint a part of the viewer
                    int oldPos = crossHairPos;
                    crossHairPos = xAt(crossHairTime);

                    int newPos = crossHairPos;

                    if (newPos >= oldPos) {
                        repaint(oldPos - 2, 0, newPos - oldPos + 4, getHeight());

                        //repaint();
                    } else {
                        repaint(newPos - 2, 0, oldPos - newPos + 4, getHeight());

                        //repaint();
                    }
                }
            }
        }
    }

    /**
     * Update method from ActiveAnnotationUser.<br>
     * The application wide active annotation corresponds to the cursorTag2D
     * in this viewer. If the Tier that the cursorTag2D belongs to is
     * invisible the cursorTag2D is <i>not</i> set to <code>null</code>.
     */
    public void updateActiveAnnotation() {
        dismissEditBox();

        Annotation anno = getActiveAnnotation();

        if (anno != null) {
            //look for the annotation
            Tier2D tier2d;
            Tag2D tag2d;
            Iterator allIter = allTiers.iterator();

            while (allIter.hasNext()) {
                tier2d = (Tier2D) allIter.next();

                if (tier2d.getTier() == anno.getTier()) {
                    Iterator tagIter = tier2d.getTags();

                    while (tagIter.hasNext()) {
                        tag2d = (Tag2D) tagIter.next();

                        if (tag2d.getAnnotation() == anno) {
                            cursorTag2D = tag2d;
                            cursorTierIndex = visibleTiers.indexOf(cursorTag2D.getTier2D());
                            ensureVerticalVisibilityOfActiveAnnotation();

                            break;
                        }
                    }
                }
            }

            // update interval //
            if (!playerIsPlaying()) {
                if ((anno.getBeginTimeBoundary() < intervalBeginTime) ||
                        (anno.getEndTimeBoundary() > intervalEndTime)) {
                    long newBeginTime = intervalBeginTime;
                    long newEndTime = intervalEndTime;

                    if ((anno.getBeginTimeBoundary() < intervalBeginTime) &&
                            (anno.getBeginTimeBoundary() > (SCROLL_OFFSET * msPerPixel))) {
                        newBeginTime = anno.getBeginTimeBoundary() -
                            (SCROLL_OFFSET * msPerPixel);
                        newEndTime = newBeginTime + (imageWidth * msPerPixel);
                    } else if (anno.getEndTimeBoundary() > intervalEndTime) {
                        newEndTime = anno.getEndTimeBoundary() +
                            (SCROLL_OFFSET * msPerPixel);
                        newBeginTime = newEndTime - (imageWidth * msPerPixel);

                        if ((newBeginTime > anno.getBeginTimeBoundary()) &&
                                (anno.getBeginTimeBoundary() > (SCROLL_OFFSET * msPerPixel))) {
                            newBeginTime = anno.getBeginTimeBoundary() -
                                (SCROLL_OFFSET * msPerPixel);
                            newEndTime = newBeginTime +
                                (imageWidth * msPerPixel);
                        } else if (newBeginTime > anno.getBeginTimeBoundary()) {
                            newBeginTime = 0;
                            newEndTime = imageWidth * msPerPixel;
                        }
                    }

                    if (timeScaleConnected) {
                        setGlobalTimeScaleIntervalBeginTime(newBeginTime);
                        setGlobalTimeScaleIntervalEndTime(newEndTime);
                    } else {
                        setLocalTimeScaleIntervalBeginTime(newBeginTime);
                    }
                }
            }

            // end update interval //
        } else {
            cursorTag2D = null;
        }

        repaint();
    }

    /**
     * Implements ACMEditListener.<br>
     * The ACMEditEvent that is received contains information about the kind
     * of modification and the objects effected by that modification.
     *
     * @param e the event object
     *
     * @see ACMEditEvent
     */
    public void ACMEdited(ACMEditEvent e) {
        //System.out.println("ACMEdited:: operation: " + e.getOperation() + ", invalidated: " + e.getInvalidatedObject());
        //System.out.println("\tmodification: " + e.getModification() + ", source: " + e.getSource());
        switch (e.getOperation()) {
        case ACMEditEvent.ADD_TIER:

            if (e.getModification() instanceof TierImpl) {
                tierAdded((TierImpl) e.getModification());

                if (multiTierControlPanel != null) {
                    multiTierControlPanel.tierAdded((TierImpl) e.getModification());
                }
            }

            break;

        case ACMEditEvent.REMOVE_TIER:

            if (e.getModification() instanceof TierImpl) {
                tierRemoved((TierImpl) e.getModification());

                if (multiTierControlPanel != null) {
                    multiTierControlPanel.tierRemoved((TierImpl) e.getModification());
                }
            }

            break;

        case ACMEditEvent.CHANGE_TIER:

            if (e.getInvalidatedObject() instanceof TierImpl) {
                tierChanged((TierImpl) e.getInvalidatedObject());

                if (multiTierControlPanel != null) {
                    multiTierControlPanel.tierChanged((TierImpl) e.getInvalidatedObject());
                }
            }

            break;

        // if i'm right for the next three operations the event's
        // invalidated Object should be a tier
        // and the modification object the new annotation...
        case ACMEditEvent.ADD_ANNOTATION_HERE:

            if (e.getInvalidatedObject() instanceof TierImpl &&
                    e.getModification() instanceof Annotation) {
                //annotationAdded((TierImpl)e.getInvalidatedObject(), (Annotation)e.getModification());
                // to accomodate right updates in bulldozer mode stupidly reextract all dependant tiers
                TierImpl invTier = (TierImpl) e.getInvalidatedObject();
                Vector depTiers = invTier.getDependentTiers();

                if (depTiers == null) {
                    depTiers = new Vector();
                }

                depTiers.add(0, invTier);
                annotationsAdded(depTiers);

                //setActiveAnnotation((Annotation) e.getModification());
                showEditBoxForAnnotation((Annotation) e.getModification());

                if (editBox.isVisible()) {
                    editBox.requestFocus();
                }
            }

            break;

        case ACMEditEvent.ADD_ANNOTATION_BEFORE:

        // fall through
        //break;
        case ACMEditEvent.ADD_ANNOTATION_AFTER:

            if (e.getInvalidatedObject() instanceof TierImpl &&
                    e.getModification() instanceof Annotation) {
                /*
                   TierImpl invTier = (TierImpl) e.getInvalidatedObject();
                   Vector depTiers = invTier.getDependentTiers(userIdentity);

                                   if (depTiers == null) {
                                       depTiers = new Vector();
                                   }

                                   depTiers.add(0, invTier);
                                   annotationsAdded(depTiers);
                 */

                //setActiveAnnotation((Annotation) e.getModification());
                // jul 2004: redo all; we can not rely on the fact that only dependent
                // tiers will be effected by this operation...
                // (problem: unaligned annotations on time-subdivision tiers)
                Vector tiers = transcription.getTiers();

                annotationsAdded(tiers);
                showEditBoxForAnnotation((Annotation) e.getModification());

                if (editBox.isVisible()) {
                    editBox.requestFocus();
                }

                //maybe this could be more finegrained by re-extracting only all
                // RefAnnotations referring to the parent of the modified annotation...
            }

            break;

        case ACMEditEvent.CHANGE_ANNOTATIONS:

            if (e.getInvalidatedObject() instanceof Transcription) {
                transcriptionChanged();
            }

            break;

        case ACMEditEvent.REMOVE_ANNOTATION:

            if (e.getInvalidatedObject() instanceof Transcription) {
                //System.out.println("Invalidated object: " + e.getInvalidatedObject());
                annotationsRemoved();
            }

            break;

        case ACMEditEvent.CHANGE_ANNOTATION_TIME:

            if (e.getInvalidatedObject() instanceof AlignableAnnotation) {
                TierImpl invTier = (TierImpl) ((AlignableAnnotation) e.getInvalidatedObject()).getTier();
                Vector depTiers = invTier.getDependentTiers();

                if (depTiers == null) {
                    depTiers = new Vector();
                }

                depTiers.add(0, invTier);
                annotationTimeChanged(depTiers);
            }

            break;

        case ACMEditEvent.CHANGE_ANNOTATION_VALUE:

            if (e.getSource() instanceof Annotation) {
                Annotation a = (Annotation) e.getSource();

                if (a.getTier() instanceof TierImpl) {
                    annotationChanged((TierImpl) a.getTier(), a);

                    // The TimeLineViewer has a special responsibility in showing an
                    // edit box when an annotation has been created.
                    // When in an undo action a deleted annotation is recreated 
                    // and the value restored in a single pass, no component seems
                    // to actually have keyboard focus: no keyboard shortcur works
                    // (can't explain this)
                    // the following code ensures that after finishing the undo action
                    // a component receives the keyboard focus in a separate thread

                    /*
                       SwingUtilities.invokeLater(new Runnable(){
                           public void run() {
                               TimeLineViewer.this.getParent().requestFocus();
                           }
                       });
                     */
                }
            }

            break;

        default:
            break;
        }
    }

    //**************************************************************************************//

    /* implement SelectionUser */
    /* (non-Javadoc)
     * @see mpi.eudico.client.annotator.SelectionUser#updateSelection()
     */
    public void updateSelection() {
        selectionBeginPos = (int) (getSelectionBeginTime() / msPerPixel);
        selectionEndPos = (int) (getSelectionEndTime() / msPerPixel);
        paintBuffer();

        //repaint();
    }

    //**************************************************************************************//

    /* implement ElanLocaleListener */

    /**
     * Update locale sensitive UI elements.
     */
    public void updateLocale() {
        if (popup != null) {
            zoomMI.setText(ElanLocale.getString("TimeScaleBasedViewer.Zoom"));
            timeScaleConMI.setText(ElanLocale.getString(
                    "TimeScaleBasedViewer.Connected"));
            activeAnnStrokeBoldMI.setText(ElanLocale.getString(
                    "TimeLineViewer.Menu.Stroke"));
            fontMenu.setText(ElanLocale.getString("Menu.View.FontSize"));
            activeTierMI.setText(ElanLocale.getString("Menu.Edit.ActiveTier"));
            deleteTierMI.setText(ElanLocale.getString("Menu.Edit.DeleteTier"));
            changeTierMI.setText(ElanLocale.getString(
                    "Menu.Edit.ChangeTierAttributes"));
            newAnnoMI.setText(ElanLocale.getString("Menu.Edit.NewAnnotation"));
            newAnnoBeforeMI.setText(ElanLocale.getString(
                    "Menu.Edit.NewAnnotationBefore"));
            newAnnoAfterMI.setText(ElanLocale.getString(
                    "Menu.Edit.NewAnnotationAfter"));
            modifyAnnoMI.setText(ElanLocale.getString(
                    "Menu.Edit.ModifyAnnotation"));

            //modifyAnnoTimeMI.setText(ElanLocale.getString(
            //       "Menu.Edit.ModifyAnnotationTime"));
            deleteAnnoMI.setText(ElanLocale.getString(
                    "Menu.Edit.DeleteAnnotation"));
        }

        if (editBox != null) {
            editBox.updateLocale();
        }
    }

    /**
     * Updates the font that is used in the visualization of the annotations<br>
     * Does not change the font in the time ruler.
     *
     * @param f the new Font
     */
    public void updateFont(Font f) {
        int oldSize = font.getSize();
        font = f;
        setFont(font);
        tooltipFont = font.deriveFont((float) tooltipFontSize);
        metrics = getFontMetrics(font);
        recalculateTagSizes();

        //pixelsForTierHeight = font.getSize() * 3;
        pixelsForTierHeight = font.getSize() + 24;

        if (oldSize != f.getSize()) {
            notifyMultiTierControlPanel();
            paintBuffer();
            scrollBar.setBlockIncrement(pixelsForTierHeight);
            updateScrollBar();
        } else {
            paintBuffer();
        }
    }

    /**
     * Sets the font size.
     *
     * @param fontSize the new font size
     */
    public void setFontSize(int fontSize) {
        updateFont(getFont().deriveFont((float) fontSize));

        if (popup != null) {
            Enumeration en = fontSizeBG.getElements();
            JMenuItem item;
            String value;

            while (en.hasMoreElements()) {
                item = (JMenuItem) en.nextElement();
                value = item.getText();

                try {
                    int v = Integer.parseInt(value);

                    if (v == fontSize) {
                        item.setSelected(true);

                        //updateFont(getFont().deriveFont((float) fontSize));
                        break;
                    }
                } catch (NumberFormatException nfe) {
                    //// do nothing
                }
            }
        }
    }

    /**
     * Returns the current font size.
     *
     * @return the current font size
     */
    public int getFontSize() {
        return font.getSize();
    }

    /**
     * Handle scrolling of the viewer image.
     *
     * @param e DOCUMENT ME!
     */
    public void adjustmentValueChanged(AdjustmentEvent e) {
        int value = e.getValue();

        if (editBox.isVisible()) {
            Point p = editBox.getLocation();
            p.y += (verticalScrollOffset - value);
            editBox.setLocation(p);
        }

        setVerticalScrollOffset(value);
        notifyMultiTierControlPanel();
    }

    //**************************************************************************************//

    /* implement MultiTierViewer */
    public void setVisibleTiers(Vector tiers) {
        //store some old values
        dismissEditBox();

        int oldNum = visibleTiers.size();

        synchronized (tierLock) {
            visibleTiers.clear();

            Tier2D t2d;
            Tier tier;

            Enumeration en = tiers.elements();

            while (en.hasMoreElements()) {
                tier = (Tier) en.nextElement();

                Iterator it = allTiers.iterator();

                while (it.hasNext()) {
                    t2d = (Tier2D) it.next();

                    if (t2d.getTier() == tier) {
                        visibleTiers.add(t2d);

                        break;
                    }
                }
            }
        }

        if (cursorTag2D != null) {
            cursorTierIndex = visibleTiers.indexOf(cursorTag2D.getTier2D());
        }

        notifyMultiTierControlPanel();
        paintBuffer();

        if (oldNum != visibleTiers.size()) {
            updateScrollBar();
        }
    }

    /**
     * DOCUMENT ME!
     *
     * @param tier DOCUMENT ME!
     */
    public void setActiveTier(Tier tier) {
        Iterator it = allTiers.iterator(); //visibleTiers??
        Tier2D t2d;

        while (it.hasNext()) {
            t2d = (Tier2D) it.next();

            if (t2d.getTier() == tier) {
                t2d.setActive(true);
            } else {
                t2d.setActive(false);
            }
        }

        paintBuffer();
    }

    /**
     * DOCUMENT ME!
     *
     * @param controller DOCUMENT ME!
     */
    public void setMultiTierControlPanel(MultiTierControlPanel controller) {
        multiTierControlPanel = controller;

        //paintBuffer();
        notifyMultiTierControlPanel();
    }

    //*************************************************************************************//

    /* implement ComponentListener */
    /*
     * Calculate a new BufferedImage taken the new size of the Component
     */
    public void componentResized(ComponentEvent e) {
        paintBuffer();

        //scrollBar.setVisible(true);
        scrollBar.setBounds(getWidth() - defBarWidth, 0, defBarWidth,
            getHeight());
        scrollBar.revalidate();
        updateScrollBar();

        /*
           intervalEndTime = intervalBeginTime + imageWidth * msPerPixel;
           if (timeScaleConnected) {
               setGlobalTimeScaleIntervalEndTime(intervalEndTime);
           }
         */
    }

    /**
     * DOCUMENT ME!
     *
     * @param e DOCUMENT ME!
     */
    public void componentMoved(ComponentEvent e) {
    }

    /**
     * DOCUMENT ME!
     *
     * @param e DOCUMENT ME!
     */
    public void componentShown(ComponentEvent e) {
        paintBuffer();
        scrollBar.setBounds(getWidth() - defBarWidth, 0, defBarWidth,
            getHeight());
        scrollBar.revalidate();
        updateScrollBar();
    }

    /**
     * DOCUMENT ME!
     *
     * @param e DOCUMENT ME!
     */
    public void componentHidden(ComponentEvent e) {
    }

    //***********************************************************************************

    /* implement MouseListener and MouseMotionListener
       /*
     * A mouse click in the SignalViewer updates the media time
     * to the time corresponding to the x-position.
     */
    public void mouseClicked(MouseEvent e) {
        if (SwingUtilities.isRightMouseButton(e) || e.isPopupTrigger()) {
            return;
        }

        Annotation annotation = null; // new code by AR
        boolean shouldShowEditBox = false;

        // grab keyboard focus
        requestFocus();

        Point pp = e.getPoint();

        if ((e.getClickCount() == 1) && e.isShiftDown()) {
            // change the selection interval
            if (getSelectionBeginTime() != getSelectionEndTime()) {
                long clickTime = timeAt(pp.x);

                if (clickTime > getSelectionEndTime()) {
                    // expand to the right
                    setSelection(getSelectionBeginTime(), clickTime);
                } else if (clickTime < getSelectionBeginTime()) {
                    // expand to the left
                    setSelection(clickTime, getSelectionEndTime());
                } else {
                    // reduce from left or right, whichever boundary is closest
                    // to the click time
                    if ((clickTime - getSelectionBeginTime()) < (getSelectionEndTime() -
                            clickTime)) {
                        setSelection(clickTime, getSelectionEndTime());
                    } else {
                        setSelection(getSelectionBeginTime(), clickTime);
                    }
                }
            }
        } else {
            Point inverse = new Point(pp);
            dismissEditBox();

            if (rulerAlwaysVisible && (pp.y < rulerHeight)) {
                //cursorTag2D = null;
            } else {
                //compensate for the intervalBeginTime
                inverse.x += timeToPixels(intervalBeginTime);
                inverse.y += verticalScrollOffset;
                cursorTierIndex = getTierIndexForPoint(inverse);
                cursorTag2D = getTagAt(inverse, cursorTierIndex);

                if (cursorTag2D != null) {
                    annotation = cursorTag2D.getAnnotation();
                    setActiveAnnotation(annotation);

                    //					disabled by AR
                    //					setActiveAnnotation(((DobesTag)cursorTag2D.getTag()).getAnnotation());
                } else {
                    setActiveAnnotation(null);

                    //setMediaTime(timeAt(pp.x));
                }

                if ((e.getClickCount() == 2) && (cursorTag2D != null)) {
                    //showEditBoxForTag(cursorTag2D);
                    shouldShowEditBox = true;

                    if (e.isShiftDown()) {
                        forceOpenControlledVocabulary = true;
                    } else {
                        forceOpenControlledVocabulary = false;
                    }
                } else if ((e.getClickCount() >= 2) && (cursorTag2D == null)) {
                    autoInsertAnnotation(inverse, cursorTierIndex);
                }
            }

            //repaint();
            // disabled by AR
            //			setMediaTime(timeAt(pp.x));
            // new code by AR, takes care of setting selection in every mode to boundaries of active annotation
            if ((annotation == null) && !e.isAltDown()) {
                setMediaTime(timeAt(pp.x));
            }

            if (shouldShowEditBox && (cursorTag2D != null)) {
                showEditBoxForTag(cursorTag2D);
            }
        }
    }

    /*
     * @see java.awt.event.MouseListener#mousePressed(java.awt.event.MouseEvent)
     */
    public void mousePressed(MouseEvent e) {
        Point pp = e.getPoint();

        // HS nov 04: e.isPopupTrigger always returns false on my PC...
        if (SwingUtilities.isRightMouseButton(e) || e.isPopupTrigger()) {
            if (popup == null) {
                createPopupMenu();
            }

            updatePopup(pp);

            if (e.isShiftDown()) {
                forceOpenControlledVocabulary = true;
            } else {
                forceOpenControlledVocabulary = false;
            }

            if ((popup.getWidth() == 0) || (popup.getHeight() == 0)) {
                popup.show(this, pp.x, pp.y);
            } else {
                popup.show(this, pp.x, pp.y);
                SwingUtilities.convertPointToScreen(pp, this);

                Dimension d = Toolkit.getDefaultToolkit().getScreenSize();
                Window w = SwingUtilities.windowForComponent(this);

                if ((pp.x + popup.getWidth()) > d.width) {
                    pp.x -= popup.getWidth();
                }

                //this does not account for a desktop taskbar
                if ((pp.y + popup.getHeight()) > d.height) {
                    pp.y -= popup.getHeight();
                }

                //keep it in the window then
                if ((pp.y + popup.getHeight()) > (w.getLocationOnScreen().y +
                        w.getHeight())) {
                    pp.y -= popup.getHeight();
                }

                popup.setLocation(pp);
            }

            return;
        }

        if (playerIsPlaying()) {
            stopPlayer();
        }

        dragStartPoint = e.getPoint();
        dragStartTime = timeAt(dragStartPoint.x);

        if (e.isAltDown() && rulerAlwaysVisible &&
                (dragStartPoint.y < rulerHeight)) {
            panMode = true;
            setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));

            //dismissEditBox();
        } else if (e.isAltDown()) {
            // drag-edit an annotation
            if ((cursorTag2D != null) && (dragEditTag2D == null)) {
                if (getHoverTag(new Point(e.getPoint())) == cursorTag2D) {
                    dragEditTag2D = cursorTag2D;
                }
            }

            if (dragEditTag2D != null) {
                // before changing anything, create a copy
                Tag2D copy = new Tag2D(dragEditTag2D.getAnnotation());
                copy.setX(dragEditTag2D.getX());
                copy.setWidth(dragEditTag2D.getWidth());
                dragEditTag2D = copy;
                dragEditing = true;

                // find the parent annotation's boundaries, if this one is not on 
                // a root tier
                if (((TierImpl) dragEditTag2D.getAnnotation().getTier()).hasParentTier()) {
                    //TierImpl pt = (TierImpl) ((TierImpl)dragEditTag2D.getAnnotation().getTier()).getParentTier();
                    //AlignableAnnotation pa = (AlignableAnnotation) pt.getAnnotationAtTime(
                    //	dragEditTag2D.getAnnotation().getBeginTimeBoundary());
                    AlignableAnnotation pa = (AlignableAnnotation) dragEditTag2D.getAnnotation()
                                                                                .getParentAnnotation();

                    if (pa != null) {
                        dragParentBegin = pa.getBeginTimeBoundary();
                        dragParentEnd = pa.getEndTimeBoundary();
                    } else {
                        dragParentBegin = -1L;
                        dragParentEnd = -1L;
                    }
                } else {
                    dragParentBegin = -1L;
                    dragParentEnd = -1L;
                }

                int x = (int) ((dragEditTag2D.getBeginTime() / msPerPixel) -
                    (intervalBeginTime / msPerPixel));
                int x2 = x +
                    (int) ((dragEditTag2D.getEndTime() -
                    dragEditTag2D.getBeginTime()) / msPerPixel);

                if (Math.abs(x - e.getX()) < DRAG_EDIT_MARGIN) {
                    dragEditMode = DRAG_EDIT_LEFT;
                    setCursor(Cursor.getPredefinedCursor(Cursor.W_RESIZE_CURSOR));
                } else if (Math.abs(x2 - e.getX()) < DRAG_EDIT_MARGIN) {
                    dragEditMode = DRAG_EDIT_RIGHT;
                    setCursor(Cursor.getPredefinedCursor(Cursor.E_RESIZE_CURSOR));
                } else {
                    dragEditMode = DRAG_EDIT_CENTER;
                    setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
                }

                if ((x2 - x) < (3 * DRAG_EDIT_MARGIN)) {
                    dragEditMode = DRAG_EDIT_CENTER;
                    setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
                }
            }
        } else {
            panMode = false;

            /* just to be sure a running scroll thread can be stopped */
            stopScroll();
        }
    }

    /*
     * @see java.awt.event.MouseListener#mouseReleased(java.awt.event.MouseEvent)
     */
    public void mouseReleased(MouseEvent e) {
        //stop scrolling thread
        stopScroll();

        // changing the selection might have changed the intervalBeginTime
        if (timeScaleConnected) {
            setGlobalTimeScaleIntervalBeginTime(intervalBeginTime);
            setGlobalTimeScaleIntervalEndTime(intervalEndTime);
        }

        if (panMode) {
            panMode = false;
            setCursor(Cursor.getDefaultCursor());
        }

        if (dragEditing) {
            setCursor(Cursor.getDefaultCursor());
            doDragModifyAnnotationTime();
            dragEditing = false;
            dragEditTag2D = null;
            dragParentBegin = -1L;
            dragParentEnd = -1L;
            repaint();
        }
    }

    /*
     * @see java.awt.event.MouseListener#mouseEntered(java.awt.event.MouseEvent)
     */
    public void mouseEntered(MouseEvent e) {
    }

    /*
     * @see java.awt.event.MouseListener#mouseExited(java.awt.event.MouseEvent)
     */
    public void mouseExited(MouseEvent e) {
        //stop scrolling thread
        stopScroll();
        hoverTag2D = null;
        showEmptySlots = false;
        repaint();
    }

    /*
     * @see java.awt.event.MouseMotionListener#mouseDragged(java.awt.event.MouseEvent)
     */
    public void mouseDragged(MouseEvent e) {
        if (SwingUtilities.isRightMouseButton(e)) {
            return;
        }

        dragEndPoint = e.getPoint();

        //panning
        if (panMode) {
            int scrolldiff = dragEndPoint.x - dragStartPoint.x;

            // some other viewer may have a media offset...
            long newTime = intervalBeginTime - (scrolldiff * msPerPixel);

            if ((intervalBeginTime < 0) && (newTime < intervalBeginTime)) {
                newTime = intervalBeginTime;
            }

            setIntervalBeginTime(newTime);
            dragStartPoint = dragEndPoint;

            return;
        }

        /*e.getPoint can be outside the image size*/
        if ((dragEndPoint.x <= 0) ||
                (dragEndPoint.x >= (getWidth() - defBarWidth))) {
            stopScroll();

            /*
               if (timeScaleConnected) {
                   setGlobalTimeScaleIntervalBeginTime(intervalBeginTime);
                   setGlobalTimeScaleIntervalEndTime(intervalEndTime);
               }
             */
            return;
        }

        //auto scroll first
        if ((dragEndPoint.x < SCROLL_OFFSET) && (dragEndPoint.x > 0)) {
            /*
               long begin = intervalBeginTime - SCROLL_OFFSET * msPerPixel;
               if (begin < 0) {
                   begin = 0;
               }
               setIntervalBeginTime(begin);
               //paintBuffer();
             */
            if (scroller == null) {
                // if the dragging starts close to the edge call setSelection
                if ((dragStartPoint.x < SCROLL_OFFSET) &&
                        (dragStartPoint.x > 0)) {
                    setSelection(dragStartTime, dragStartTime);
                }

                stopScrolling = false;
                scroller = new DragScroller(-SCROLL_OFFSET / 4, 30);
                scroller.start();
            }

            return;
        } else if ((dragEndPoint.x > (getWidth() - defBarWidth - SCROLL_OFFSET)) &&
                (dragEndPoint.x < (getWidth() - defBarWidth))) {
            /*
               long begin = intervalBeginTime + SCROLL_OFFSET * msPerPixel;
               setIntervalBeginTime(begin);
               //paintBuffer();
             */
            if (scroller == null) {
                // if the dragging starts close to the edge call setSelection
                if ((dragStartPoint.x > (getWidth() - defBarWidth -
                        SCROLL_OFFSET)) &&
                        (dragStartPoint.x < (getWidth() - defBarWidth))) {
                    setSelection(dragStartTime, dragStartTime);
                }

                stopScrolling = false;
                scroller = new DragScroller(SCROLL_OFFSET / 4, 30);
                scroller.start();
            }

            return;
        } else {
            stopScroll();

            if (dragEditing) {
                // don't change the selection
                updateDragEditTag(dragEndPoint);

                return;
            }

            if (timeAt(dragEndPoint.x) > dragStartTime) { //left to right
                selectionEndTime = timeAt(dragEndPoint.x);

                if (selectionEndTime > getMediaDuration()) {
                    selectionEndTime = getMediaDuration();
                }

                selectionBeginTime = dragStartTime;

                if (selectionBeginTime < 0) {
                    selectionBeginTime = 0L;
                }

                if (selectionEndTime < 0) {
                    selectionEndTime = 0L;
                }

                setMediaTime(selectionEndTime);
            } else { //right to left
                selectionBeginTime = timeAt(dragEndPoint.x);

                if (selectionBeginTime > getMediaDuration()) {
                    selectionBeginTime = getMediaDuration();
                }

                selectionEndTime = dragStartTime;

                if (selectionEndTime > getMediaDuration()) {
                    selectionEndTime = getMediaDuration();
                }

                if (selectionBeginTime < 0) {
                    selectionBeginTime = 0L;
                }

                if (selectionEndTime < 0) {
                    selectionEndTime = 0L;
                }

                setMediaTime(selectionBeginTime);
            }

            setSelection(selectionBeginTime, selectionEndTime);
            repaint();
        }
    }

    /*
     * Note: if the alt key is released while the mouse is moving
     * isAltDown() still returns true.
     * @see java.awt.event.MouseMotionListener#mouseMoved(java.awt.event.MouseEvent)
     */
    public void mouseMoved(MouseEvent e) {
        Point pp = e.getPoint(); //pp is in component coordinates
        hoverTag2D = null;
        dragEditTag2D = null;

        if (rulerAlwaysVisible && (pp.y < rulerHeight)) {
            //setToolTipText(null);
            setToolTipText(TimeFormatter.toString(timeAt(e.getPoint().x)));
            showEmptySlots = false;
            repaint();

            return;
        }

        Point inverse = new Point(pp);

        //compensate for the intervalBeginTime
        //inverse.x += timeToPixels(intervalBeginTime);
        //inverse.y += verticalScrollOffset;
        //hoverTierIndex = getTierIndexForPoint(inverse);
        //hoverTag2D = getTagAt(inverse, hoverTierIndex);
        hoverTag2D = getHoverTag(inverse);

        /*
           if (e.isShiftDown()) {
               showEmptySlots = true;
           } else {
               showEmptySlots = false;
           }
         */

        // repaint();
        if ((hoverTag2D != null) && (hoverTag2D == cursorTag2D) &&
                e.isAltDown()) {
            setToolTipText(null);

            if (cursorTag2D.getAnnotation() instanceof AlignableAnnotation) {
                // here it is for display only, don't copy 
                dragEditTag2D = cursorTag2D;
            }
        } else if (hoverTag2D != null) {
            StringBuffer sb = new StringBuffer();

            if (hoverTag2D.getAnnotation() instanceof AlignableAnnotation) {
                sb.append("BT: ");
                sb.append(TimeFormatter.toString(hoverTag2D.getBeginTime()));
                sb.append(", ET: ");
                sb.append(TimeFormatter.toString(hoverTag2D.getEndTime()));
                sb.append(" ");
            }

            sb.append(hoverTag2D.getValue());

            //make tooltip multiline
            final int MAXTOOLTIPLENGTH = 140;
            String strTemp = sb.toString();
            String strEnd = "<html>";

            while (strTemp.length() > MAXTOOLTIPLENGTH) {
                int index = strTemp.lastIndexOf(" ", MAXTOOLTIPLENGTH);
                strEnd += (strTemp.substring(0, index) + "<br>");
                strTemp = strTemp.substring(index);
            }

            strEnd += (strTemp + "</html>");
            setToolTipText(strEnd);

            //setToolTipText(t.toString().replace('\n', ' '));
            //System.out.println("Value: " + t.getValues() + "begin: " + t.getBeginTime() + " end: " + t.getEndTime());
        } else {
            setToolTipText(null);
        }

        repaint();
    }

    /**
     * The use of a mousewheel needs Java 1.4!<br>
     * The scroll amount of the mousewheel is the height of a tier.
     *
     * @param e DOCUMENT ME!
     */
    public void mouseWheelMoved(MouseWheelEvent e) {
        if (e.getUnitsToScroll() > 0) {
            scrollBar.setValue(scrollBar.getValue() + pixelsForTierHeight);
        } else {
            scrollBar.setValue(scrollBar.getValue() - pixelsForTierHeight);
        }
    }

    /**
     * DOCUMENT ME!
     *
     * @param e DOCUMENT ME!
     */
    public void keyTyped(KeyEvent e) {
    }

    /**
     * DOCUMENT ME!
     *
     * @param e DOCUMENT ME!
     */
    public void keyPressed(KeyEvent e) {
        if (e.getModifiers() == (KeyEvent.ALT_MASK +
                Toolkit.getDefaultToolkit().getMenuShortcutKeyMask() +
                KeyEvent.SHIFT_MASK)) {
            if (!showEmptySlots) {
                showEmptySlots = true;
                repaint();
            }
        } else {
            if (showEmptySlots) {
                showEmptySlots = false;
                repaint();
            }
        }
    }

    /**
     * DOCUMENT ME!
     *
     * @param e DOCUMENT ME!
     */
    public void keyReleased(KeyEvent e) {
        if ((e.getKeyCode() == KeyEvent.VK_CONTROL) ||
                (e.getKeyCode() == KeyEvent.VK_ALT) ||
                (e.getKeyCode() == KeyEvent.VK_SHIFT)) {
            if (showEmptySlots) {
                showEmptySlots = false;
                repaint();
            }
        }

        if (e.getKeyCode() == KeyEvent.VK_ALT) {
            if (!dragEditing) {
                dragEditTag2D = null;
                repaint();
            }
        }
    }

    /*
     * @see java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent)
     */
    public void actionPerformed(ActionEvent e) {
        if (e.getActionCommand().equals("connect")) {
            boolean connected = ((JCheckBoxMenuItem) e.getSource()).getState();
            setTimeScaleConnected(connected);
        } else if (e.getActionCommand().equals("aastroke")) {
            aaStrokeBold = ((JCheckBoxMenuItem) e.getSource()).getState();
            repaint();
        } else if (e.getActionCommand().equals("activeTier")) {
            doActiveTier();
        } else if (e.getActionCommand().equals("deleteTier")) {
            doDeleteTier();
        } else if (e.getActionCommand().equals("changeTier")) {
            doChangeTier();
        } else if (e.getActionCommand().equals("newAnn")) {
            doNewAnnotation();
        } else if (e.getActionCommand().equals("annBefore")) {
            doAnnotationBefore();
        } else if (e.getActionCommand().equals("annAfter")) {
            doAnnotationAfter();
        } else if (e.getActionCommand().equals("modifyAnn")) {
            doModifyAnnotation();
        } else if (e.getActionCommand().equals("deleteAnn")) {
            doDeleteAnnotation();
        } else if (e.getActionCommand().indexOf("font") > -1) {
            String sizeString = e.getActionCommand();
            int index = sizeString.indexOf("font") + 4;
            int size = 12;

            try {
                size = Integer.parseInt(sizeString.substring(index));
            } catch (NumberFormatException nfe) {
                System.err.println("Error parsing font size");
            }

            updateFont(getFont().deriveFont((float) size));
        } else {
            /* the rest are zoom menu items*/
            String zoomString = e.getActionCommand();
            int zoom = 100;

            try {
                zoom = Integer.parseInt(zoomString);
            } catch (NumberFormatException nfe) {
                System.err.println("Error parsing the zoom level");
            }

            int newMsPerPixel = (int) ((100f / zoom) * 10);
            setMsPerPixel(newMsPerPixel);
        }
    }

    /**
     * Scrolls the image while dragging to the left or right with the specified
     * number of pixels.<br>
     * This method is called from a separate Thread.
     *
     * @param numPixels the number of pixels to scroll the interval
     *
     * @see DragScroller
     */
    synchronized void scroll(int numPixels) {
        long begin = intervalBeginTime + (numPixels * msPerPixel);

        if (numPixels > 0) {
            // left to right, change selection while scrolling
            setIntervalBeginTime(begin);
            selectionEndTime = getSelectionEndTime() +
                (numPixels * msPerPixel);

            if (selectionEndTime > getMediaDuration()) {
                selectionEndTime = getMediaDuration();
            }

            setMediaTime(selectionEndTime);
            setSelection(getSelectionBeginTime(), selectionEndTime);
        } else {
            // right to left
            if (begin < 0) {
                begin = 0;
            }

            setIntervalBeginTime(begin);
            selectionBeginTime = getSelectionBeginTime() +
                (numPixels * msPerPixel);

            if (selectionBeginTime < 0) {
                selectionBeginTime = 0;
            }

            setMediaTime(selectionBeginTime);
            setSelection(selectionBeginTime, getSelectionEndTime());
        }

        //repaint();
    }

    /**
     * DOCUMENT ME!
     */
    void stopScroll() {
        /*
           if (scroller != null) {
               try {
                   scroller.interrupt();
               } catch (SecurityException se) {
                   System.out.println("TimeLineViewer: could not stop scroll thread");
               } finally {
               }
               scroller = null;
           }
         */
        stopScrolling = true;
        scroller = null;
    }

    // the edit actions from the popup menu, the Commands and Actions will come in here
    private void doActiveTier() {
        if ((rightClickTier == null) || (multiTierControlPanel == null)) {
            return;
        }

        multiTierControlPanel.setActiveTier(rightClickTier.getTier());
    }

    private void doDeleteTier() {
        Command c = ELANCommandFactory.createCommand(transcription,
                ELANCommandFactory.EDIT_TIER);

        Object[] args = new Object[] {
                new Integer(EditTierDialog.DELETE), rightClickTier.getTier()
            };

        c.execute(transcription, args);
    }

    private void doChangeTier() {
        Command c = ELANCommandFactory.createCommand(transcription,
                ELANCommandFactory.EDIT_TIER);

        Object[] args = new Object[] {
                new Integer(EditTierDialog.CHANGE), rightClickTier.getTier()
            };

        c.execute(transcription, args);
    }

    private void doNewAnnotation() {
        if (rightClickTier == null) {
            return;
        }

        // use command
        TierImpl tier = rightClickTier.getTier();
        long begin = getSelectionBeginTime();
        long end = getSelectionEndTime();

        if (begin == end) {
            return;
        }

        if (!tier.isTimeAlignable()) {
            begin = rightClickTime;
            end = rightClickTime;
        }

        Command c = ELANCommandFactory.createCommand(transcription,
                ELANCommandFactory.NEW_ANNOTATION);
        Object[] args = new Object[] {
                getViewerManager(), new Long(begin), new Long(end)
            };
        c.execute(tier, args);
    }

    /**
     * Annotation before is relative to the cursor tag (active annotation)
     */
    private void doAnnotationBefore() {
        if ((rightClickTier == null) || (cursorTag2D == null)) {
            return;
        }

        Command c = ELANCommandFactory.createCommand(transcription,
                ELANCommandFactory.NEW_ANNOTATION_BEFORE);
        Object[] args = new Object[] { cursorTag2D.getAnnotation() };
        c.execute(rightClickTier.getTier(), args);
    }

    /**
     * Annotation after is relative to the cursor tag (active annotation)
     */
    private void doAnnotationAfter() {
        if ((rightClickTier == null) || (cursorTag2D == null)) {
            return;
        }

        Command c = ELANCommandFactory.createCommand(transcription,
                ELANCommandFactory.NEW_ANNOTATION_AFTER);
        Object[] args = new Object[] { cursorTag2D.getAnnotation() };
        c.execute(rightClickTier.getTier(), args);
    }

    private void doModifyAnnotation() {
        if (cursorTag2D != null) {
            showEditBoxForTag(cursorTag2D);
        }
    }

    private void doDeleteAnnotation() {
        if (cursorTag2D != null) {
            TierImpl tier = cursorTag2D.getTier2D().getTier();
            Annotation aa = cursorTag2D.getAnnotation();

            Command c = ELANCommandFactory.createCommand(transcription,
                    ELANCommandFactory.DELETE_ANNOTATION);
            c.execute(tier, new Object[] { getViewerManager(), aa });
        }
    }

    /**
     * Calculate the new begin and end time of the active annotation  and
     * create a command.
     */
    private void doDragModifyAnnotationTime() {
        if ((dragEditTag2D == null) ||
                !(getActiveAnnotation() instanceof AlignableAnnotation)) {
            return;
        }

        long beginTime = 0L;
        long endTime = 0L;

        switch (dragEditMode) {
        case DRAG_EDIT_CENTER:
            beginTime = pixelToTime(dragEditTag2D.getX());
            endTime = pixelToTime(dragEditTag2D.getX() +
                    dragEditTag2D.getWidth());

            break;

        case DRAG_EDIT_LEFT:
            beginTime = pixelToTime(dragEditTag2D.getX());
            endTime = dragEditTag2D.getEndTime();

            break;

        case DRAG_EDIT_RIGHT:
            beginTime = dragEditTag2D.getBeginTime();
            endTime = pixelToTime(dragEditTag2D.getX() +
                    dragEditTag2D.getWidth());
        }

        if (endTime <= beginTime) {
            return;
        }

        Command c = ELANCommandFactory.createCommand(transcription,
                ELANCommandFactory.MODIFY_ANNOTATION_TIME);
        c.execute(getActiveAnnotation(),
            new Object[] { new Long(beginTime), new Long(endTime) });
    }

    /**
     * A Thread for scrolling near the left and right border of the viewer.
     *
     * @author HS
     * @version 1.0
     */
    class DragScroller extends Thread {
        /** Holds value of property DOCUMENT ME! */
        int numPixels;

        /** Holds value of property DOCUMENT ME! */
        long sleepTime;

        /**
         * Creates a new DragScroller instance
         *
         * @param numPixels DOCUMENT ME!
         * @param sleepTime DOCUMENT ME!
         */
        DragScroller(int numPixels, long sleepTime) {
            this.numPixels = numPixels;
            this.sleepTime = sleepTime;
        }

        /**
         * Periodically scrolls the view.
         */
        public void run() {
            while (!stopScrolling) {
                TimeLineViewer.this.scroll(numPixels);

                try {
                    sleep(sleepTime);
                } catch (InterruptedException ie) {
                    return;
                }
            }
        }
    }
}
