Package de.sciss.eisenkraut.session

Source Code of de.sciss.eisenkraut.session.DocumentFrame$ActionDropMarker

/*
*  DocumentFrame.java
*  Eisenkraut
*
*  Copyright (c) 2004-2014 Hanns Holger Rutz. All rights reserved.
*
*  This software is published under the GNU General Public License v3+
*
*
*  For further information, please contact Hanns Holger Rutz at
*  contact@sciss.de
*
*
*  Changelog:
*      25-Jan-05  created from de.sciss.meloncillo.timeline.TimelineFrame
*    22-Mar-05  fixed but in vertical zoom
*    15-Jul-05  lot's of. removed some exclusive locks (fixes overview update in catch mode)
*          ; overview loading loads one more sample frame, so display
*          is in coherence with timeline selection and TimelineAxis
*    02-Aug-05  renamed to DocumentFrame ; conforms to new document handler
*    23-Sep-05  correctly disposes overview display images ; abandons backing store
*          image approach in TimelineViewport (seems to fix memory leak)
*/

package de.sciss.eisenkraut.session;

import java.awt.BasicStroke;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Stroke;
import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.ClipboardOwner;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.prefs.PreferenceChangeEvent;
import java.util.prefs.PreferenceChangeListener;
import java.util.prefs.Preferences;
import java.text.MessageFormat;

import javax.swing.*;
import javax.swing.event.MouseInputAdapter;
import javax.swing.undo.CompoundEdit;
import javax.swing.undo.UndoableEdit;

import de.sciss.app.AbstractApplication;
import de.sciss.app.AbstractWindow;
import de.sciss.app.DynamicPrefChangeManager;
import de.sciss.app.GraphicsHandler;
import de.sciss.common.AppWindow;
import de.sciss.common.BasicApplication;
import de.sciss.common.BasicMenuFactory;
import de.sciss.common.BasicWindowHandler;
import de.sciss.common.ProcessingThread;
import de.sciss.common.ShowWindowAction;
import de.sciss.eisenkraut.Main;
import de.sciss.eisenkraut.edit.BasicCompoundEdit;
import de.sciss.eisenkraut.edit.TimelineVisualEdit;
import de.sciss.eisenkraut.gui.AbstractTool;
import de.sciss.eisenkraut.gui.AudioFileInfoPalette;
import de.sciss.eisenkraut.gui.CrossfadePanel;
import de.sciss.eisenkraut.gui.GraphicsUtil;
import de.sciss.eisenkraut.gui.MenuFactory;
import de.sciss.eisenkraut.gui.ObserverPalette;
import de.sciss.eisenkraut.gui.PeakMeterManager;
import de.sciss.eisenkraut.gui.ProgressPanel;
import de.sciss.eisenkraut.gui.RecorderDialog;
import de.sciss.eisenkraut.gui.ToolAction;
import de.sciss.eisenkraut.gui.ToolActionEvent;
import de.sciss.eisenkraut.gui.ToolActionListener;
import de.sciss.eisenkraut.gui.WaveformView;
import de.sciss.eisenkraut.io.AudioTrail;
import de.sciss.eisenkraut.io.DecimatedSonaTrail;
import de.sciss.eisenkraut.io.DecimatedTrail;
import de.sciss.eisenkraut.io.DecimatedWaveTrail;
import de.sciss.eisenkraut.io.DecimationInfo;
import de.sciss.eisenkraut.io.MarkerTrail;
import de.sciss.eisenkraut.net.SuperColliderClient;
import de.sciss.eisenkraut.net.SuperColliderPlayer;
import de.sciss.eisenkraut.realtime.Transport;
import de.sciss.eisenkraut.realtime.TransportListener;
import de.sciss.eisenkraut.realtime.TransportToolBar;
import de.sciss.eisenkraut.render.FilterDialog;
import de.sciss.eisenkraut.render.RenderPlugIn;
import de.sciss.eisenkraut.timeline.AudioTrack;
import de.sciss.eisenkraut.timeline.AudioTrackRowHeader;
import de.sciss.eisenkraut.timeline.MarkerAxis;
import de.sciss.eisenkraut.timeline.TimelineAxis;
import de.sciss.eisenkraut.timeline.TimelineEvent;
import de.sciss.eisenkraut.timeline.TimelineListener;
import de.sciss.eisenkraut.timeline.TimelineScroll;
import de.sciss.eisenkraut.timeline.TimelineToolBar;
import de.sciss.eisenkraut.timeline.Track;
import de.sciss.eisenkraut.timeline.TrackRowHeader;
import de.sciss.eisenkraut.util.PrefsUtil;
import de.sciss.gui.AbstractWindowHandler;
import de.sciss.gui.Axis;
import de.sciss.gui.ComponentBoundsRestrictor;
import de.sciss.gui.ComponentHost;
import de.sciss.gui.CoverGrowBox;
import de.sciss.gui.GUIUtil;
import de.sciss.gui.GradientPanel;
import de.sciss.gui.MenuAction;
import de.sciss.gui.MenuRoot;
import de.sciss.gui.ModificationButton;
import de.sciss.gui.PathField;
import de.sciss.gui.PeakMeter;
import de.sciss.gui.PeakMeterGroup;
import de.sciss.gui.ProgressComponent;
import de.sciss.gui.SpringPanel;
import de.sciss.gui.StretchedGridLayout;
import de.sciss.gui.TopPainter;
import de.sciss.gui.TreeExpanderButton;
import de.sciss.gui.VectorSpace;
import de.sciss.io.AudioFileDescr;
import de.sciss.io.AudioFileFormatPane;
import de.sciss.io.IOUtil;
import de.sciss.io.Marker;
import de.sciss.io.Span;
import de.sciss.timebased.Trail;
import de.sciss.util.Flag;

import org.unicode.Normalizer;

/**
@author    Hanns Holger Rutz
@version  0.70, 28-Jun-08
*/
public class DocumentFrame
extends AppWindow
implements ProgressComponent, TimelineListener,
       ClipboardOwner, ToolActionListener,
       DecimatedWaveTrail.AsyncListener,
       TransportListener, PreferenceChangeListener,
       SwingConstants
{
  protected final Session          doc;
 
    private final TimelineAxis        timeAxis;
    protected final MarkerAxis        markAxis;
    protected final TrackRowHeader      markAxisHeader;
  protected final TimelineScroll      scroll;
  protected final Transport        transport;
 
  protected Span              timelineSel;
  protected Span              timelineVis;
  protected long              timelinePos;
  protected long              timelineLen;
  protected double            timelineRate;

  private final JPanel          ggTrackPanel;
  protected final WaveformView      waveView;
  protected final ComponentHost      wavePanel;
  private final JPanel          waveHeaderPanel;
  protected final JPanel          channelHeaderPanel;
  private final JPanel          flagsPanel;
  private final JPanel          rulersPanel;
  private final JPanel          metersPanel;
  private final List            collChannelHeaders    = new ArrayList();
  protected final List          collChannelRulers    = new ArrayList();
//  private final List            collChannelMeters    = new ArrayList();
  private PeakMeter[]            channelMeters      = new PeakMeter[ 0 ];
 
  private final JLabel          lbSRC;
  protected final TreeExpanderButton    ggTreeExp;
 
  private DecimatedTrail          asyncTrail        = null;

  // --- tools ---
 
  private final   Map            tools          = new HashMap();
  private      AbstractTool      activeTool        = null;
  private final  TimelinePointerTool    pointerTool;

  // --- actions ---
  private final static String        plugInPackage      = "de.sciss.eisenkraut.render.";
  private final static String        fscapePackage      = "de.sciss.fscape.render.";

  private final ActionRevealFile      actionRevealFile;
  private final ActionNewFromSel      actionNewFromSel;
  protected final ActionClose        actionClose;
  protected final ActionSave        actionSave;
  protected final ActionSaveAs      actionSaveAs;
  private final ActionSaveAs        actionSaveCopyAs;
  private final ActionSaveAs        actionSaveSelectionAs;
  private final ActionSelectAll      actionSelectAll;
  private final MenuAction        actionProcess, actionFadeIn, actionFadeOut, actionGain,
                      actionInvert, // actionMix,
                      actionReverse, actionRotateChannels, // actionSilence,
                      actionFScNeedlehole,
                      actionDebugDump, actionDebugVerify, actionInsertRec;
  protected final ActionProcessAgain    actionProcessAgain;

  private final ActionSpanWidth      actionIncHoriz, actionDecHoriz;
  protected final ActionScroll      actionZoomAllOut;
  private final AbstractAction      actionIncVertMax, actionDecVertMax;
  private final AbstractAction      actionIncVertMin, actionDecVertMin;

  private final AbstractWindow.Adapter  winListener;

  private final JLabel          lbWriteProtected;
  private boolean              writeProtected      = false;
  protected boolean            wpHaveWarned      = false;
 
  private final ShowWindowAction      actionShowWindow;
   
  private static final String smpPtrn      = "ch.{3} @ {0,number,0}";
  private static final String timePtrn    = "ch.{3} @ {1,number,integer}:{2,number,00.000}";
  protected final MessageFormat msgCsr1    = new MessageFormat( timePtrn, Locale.US );
  protected final MessageFormat msgCsr2PCMFloat = new MessageFormat( "{4,number,0.000} ({5,number,0.00} dBFS)", Locale.US );
  protected final MessageFormat msgCsr3PCMInt  = new MessageFormat( "= {6,number,0} @ {7,number,integer}-bit int", Locale.US );
  protected final MessageFormat msgCsr2Peak  = new MessageFormat( "peak {4,number,0.000} ({5,number,0.00} dBFS)", Locale.US );
  protected final MessageFormat msgCsr3RMS  = new MessageFormat( "eff {6,number,0.000} ({7,number,0.00} dBFS)", Locale.US );
  protected int          csrInfoBits;
  protected boolean        csrInfoIsInt;
  protected static final double TWENTYDIVLOG10 = 20 / Math.log( 10 );

  private final Color colrClear        = new Color( 0xA0, 0xA0, 0xA0, 0x00 );
 
  // --------- former viewport ---------
  // --- painting ---
  private final Color colrSelection      = GraphicsUtil.colrSelection;
  private final Color colrSelection2      = new Color( 0x00, 0x00, 0x00, 0x20 )// selected timeline span over unselected trns
  protected final Color colrPosition      = new Color( 0xFF, 0x00, 0x00, 0x7F );
  protected final Color colrZoom        = new Color( 0xA0, 0xA0, 0xA0, 0x7F );
  protected Rectangle  vpRecentRect      = new Rectangle();
  protected int    vpPosition        = -1;
  private Rectangle   vpPositionRect      = new Rectangle();
  protected final ArrayList vpSelections    = new ArrayList();
  protected final ArrayList vpSelectionColors  = new ArrayList();
  protected Rectangle  vpSelectionRect      = new Rectangle();
 
  private Rectangle   vpUpdateRect      = new Rectangle();
  protected Rectangle  vpZoomRect        = null;
  private float[]    vpDash          = { 3.0f, 5.0f };
  private float    vpScale;

  protected final Stroke[] vpZoomStroke      = {
    new BasicStroke( 2f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_MITER, 1.0f, vpDash, 0.0f ),
    new BasicStroke( 2f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_MITER, 1.0f, vpDash, 4.0f ),
    new BasicStroke( 2f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_MITER, 1.0f, vpDash, 6.0f ),
  };
  protected int    vpZoomStrokeIdx      = 0;

  protected boolean  waveExpanded      = true// XXX should keep that in some prefs
  protected boolean  viewMarkers;
  protected boolean  markVisible;
  private boolean    chanMeters        = false;
  private boolean    forceMeters        = false;
 
  protected final TimelineToolBar      timeTB;
  private final TransportToolBar      transTB;

  // --- progress bar ---
 
  private final JLabel /* JTextField */ ggAudioFileDescr;
  private final ProgressPanel        pProgress;
  private final CrossfadePanel      pOverlay;

  private final boolean          internalFrames;

  protected final BasicApplication    app;
  private final SuperColliderClient    superCollider;
  private final PeakMeterManager      lmm;

  protected boolean            disposed    = false;
 
  private final Timer            playTimer;
  private double              playRate    = 1.0;
 
  protected final ComponentBoundsRestrictor cbr;
 
  private static Point          lastLeftTop    = new Point();
  private static final String        KEY_TRACKSIZE  = "tracksize";
 
  private int                verticalScale;
 
  protected static final Cursor[]      zoomCsr;
 
  static {
    final Toolkit tk    = Toolkit.getDefaultToolkit();
    final Point   hotSpot  = new Point( 6, 6 );
    zoomCsr          = new Cursor[] {
      tk.createCustomCursor( tk.createImage(
          ToolAction.class.getResource( "zoomin.png" )), hotSpot, "zoom-in" ),
      tk.createCustomCursor( tk.createImage(
        ToolAction.class.getResource( "zoomout.png" )), hotSpot, "zoom-out" )
    };
  }

  /**
   *  Constructs a new timeline window with
   *  all the sub elements. Installs the
   *  global key commands. (a DocumentFrame
   *  should be created only once in the application).
   *
   *  @param  doc    session Session
   */
  public DocumentFrame( final Session doc )
  {
    super( REGULAR );
   
    app          = (BasicApplication) AbstractApplication.getApplication();

    this.doc      = doc;
    transport      = doc.getTransport();
    timelinePos      = doc.timeline.getPosition();
    timelineSel      = doc.timeline.getSelectionSpan();
    timelineVis      = doc.timeline.getVisibleSpan();
    timelineRate    = doc.timeline.getRate();
    timelineLen      = doc.timeline.getLength();
   
    superCollider    = SuperColliderClient.getInstance();

    lmm          = new PeakMeterManager( superCollider.getMeterManager() );

    final Container          cp      = getContentPane();
    final InputMap          imap    = getInputMap( JComponent.WHEN_IN_FOCUSED_WINDOW );
    final ActionMap          amap    = getActionMap();
    final AbstractButton      ggAudioInfo, ggRevealFile;
    final int            myMeta    = BasicMenuFactory.MENU_SHORTCUT == InputEvent.CTRL_MASK ?
      InputEvent.CTRL_MASK | InputEvent.SHIFT_MASK : BasicMenuFactory.MENU_SHORTCUT;  // META on Mac, CTRL+SHIFT on PC
    final TopPainter        trackPainter;
    final MenuRoot          mr;
    final JPanel          topPane    = GUIUtil.createGradientPanel();
    Box                box;

    internalFrames    = app.getWindowHandler().usesInternalFrames();

    timeTB    = new TimelineToolBar( doc );
    transTB    = new TransportToolBar( doc );

    wavePanel      = new ComponentHost();
        timeAxis      = new TimelineAxis( doc, wavePanel );
    markAxis      = new MarkerAxis( doc, wavePanel );
    viewMarkers      = app.getUserPrefs().getBoolean( PrefsUtil.KEY_VIEWMARKERS, false );
    markVisible      = viewMarkers && waveExpanded;
    markAxisHeader    = new TrackRowHeader( doc.markerTrack, doc.tracks, doc.selectedTracks, doc.getUndoManager() );
    markAxisHeader.setPreferredSize( new Dimension( 63, markAxis.getPreferredSize().height ))// XXX
    markAxisHeader.setMaximumSize( new Dimension( 128, markAxis.getMaximumSize().height ));    // XXX
    if( markVisible ) {
      markAxis.startListening();
    } else {
      markAxis.setVisible( false );
      markAxisHeader.setVisible( false );
    }
    flagsPanel      = new JPanel( new StretchedGridLayout( 0, 1, 1, 1 ));
    metersPanel      = new JPanel( new StretchedGridLayout( 0, 1, 1, 1 )); // SpringPanel( 0, 0, 1, 1 );
    rulersPanel      = new JPanel( new StretchedGridLayout( 0, 1, 1, 1 ));
    lmm.setDynamicComponent( metersPanel );
    waveHeaderPanel    = new JPanel( new BorderLayout() );
    channelHeaderPanel  = new JPanel();
    channelHeaderPanel.setLayout( new BoxLayout( channelHeaderPanel, BoxLayout.X_AXIS ));
final Box bbb = Box.createVerticalBox();
final GradientPanel gp = GUIUtil.createGradientPanel();
gp.setBottomBorder( true );
gp.setLayout( null );
gp.setPreferredSize( new Dimension( 0, timeAxis.getPreferredSize().height ));
bbb.add( gp );
bbb.add( markAxisHeader );
    waveHeaderPanel.add( bbb, BorderLayout.NORTH );
    channelHeaderPanel.add( flagsPanel );
    channelHeaderPanel.add( metersPanel );
    channelHeaderPanel.add( rulersPanel );
    waveHeaderPanel.add( channelHeaderPanel, BorderLayout.CENTER );

    waveView      = new WaveformView( doc, wavePanel );
    wavePanel.setLayout( new BoxLayout( wavePanel, BoxLayout.Y_AXIS ));
    wavePanel.add( timeAxis );
    wavePanel.add( markAxis );
    wavePanel.add( waveView );

        scroll        = new TimelineScroll( doc );
    ggTrackPanel    = new JPanel( new BorderLayout() );
    ggTrackPanel.add( wavePanel, BorderLayout.CENTER );
    ggTrackPanel.add( waveHeaderPanel, BorderLayout.WEST );
    ggTrackPanel.add( scroll, BorderLayout.SOUTH );
       
    lbWriteProtected  = new JLabel();
    ggAudioInfo      = new ModificationButton( ModificationButton.SHAPE_INFO );
    ggAudioInfo.setAction( new ActionAudioInfo() );
    ggRevealFile    = new ModificationButton( ModificationButton.SHAPE_REVEAL );
    actionRevealFile  = new ActionRevealFile();
    ggRevealFile.setAction( actionRevealFile );
        //    ggAudioFileDescr  = new JTextField( 32 );
        //    ggAudioFileDescr.setEditable( false );
        //    ggAudioFileDescr.setFocusable( false );
        //    ggAudioFileDescr.setBackground( null );
        //    ggAudioFileDescr.setBorder( null );
        ggAudioFileDescr  = new JLabel();

    lbSRC        = new JLabel( getResourceString( "buttonSRC" ));
    lbSRC.setForeground( colrClear );
    box          = Box.createHorizontalBox();
    box.add( Box.createHorizontalStrut( 4 ));
    box.add( lbWriteProtected );
    box.add( ggAudioInfo );
    if (internalFrames) box.add( ggRevealFile );
    box.add( Box.createHorizontalStrut( 4 ));
   
    pProgress      = new ProgressPanel();
    pOverlay      = new CrossfadePanel();
    pOverlay.setComponentA( ggAudioFileDescr );
    pOverlay.setComponentB( pProgress );
    box.add( pOverlay );
   
    box.add( Box.createHorizontalStrut( 4 ));
    box.add( lbSRC );
    box.add( CoverGrowBox.create( 2, 0 ));

    updateAFDGadget();
    updateCursorFormat();

// ----- afr export -----
    final JButton ggExportAFR = new JButton( getResourceString( "buttonDragRegion" ), new ImageIcon( getClass().getResource( "dragicon.png" )));
    ggExportAFR.setTransferHandler( new AFRTransferHandler() );
    final MouseInputAdapter expAFRmia = new MouseInputAdapter() {
      private MouseEvent dndInit = null;
      private boolean dndStarted = false;

      public void mousePressed( MouseEvent e )
      {
        dndInit    = e;
        dndStarted  = false;
      }
     
      public void mouseReleased( MouseEvent e )
      {
        dndInit    = null;
        dndStarted  = false;
      }
     
      public void mouseDragged( MouseEvent e )
      {
        if( !dndStarted && (dndInit != null) &&
          ((Math.abs( e.getX() - dndInit.getX() ) > 5) ||
           (Math.abs( e.getY() - dndInit.getY() ) > 5))) {
     
          JComponent c = (JComponent) e.getSource();
          c.getTransferHandler().exportAsDrag( c, e, TransferHandler.COPY );
          dndStarted = true;
        }
      }
    };
   
    ggExportAFR.addMouseListener( expAFRmia );
    ggExportAFR.addMouseMotionListener( expAFRmia );

    timeTB.add( Box.createHorizontalStrut( 4 ));
    timeTB.addButton( ggExportAFR );
// ----------
   
    topPane.setBorder( BorderFactory.createEmptyBorder( 2, 2, 2, 2 ));
    timeTB.setOpaque( false );
    topPane.add( timeTB );
    transTB.setOpaque( false );
    topPane.add( transTB );
    topPane.add( Box.createHorizontalGlue() );
    cbr      = new ComponentBoundsRestrictor();
    ggTreeExp  = new TreeExpanderButton();
    ggTreeExp.setExpandedToolTip( getResourceString( "buttonExpWaveTT" ));
    ggTreeExp.setCollapsedToolTip( getResourceString( "buttonCollWaveTT" ));
    ggTreeExp.setExpanded( true );
    ggTreeExp.addActionListener( new ActionListener() {
      public void actionPerformed( ActionEvent e )
      {
        final Dimension d  = getSize();
       
        waveExpanded  = ggTreeExp.isExpanded();
        markVisible    = viewMarkers && waveExpanded;
       
        if( waveExpanded ) {
          cbr.remove( getWindow() );
          waveView.setVisible( true );
          channelHeaderPanel.setVisible( true );
          if( viewMarkers ) {
            markAxis.setVisible( true );
            markAxisHeader.setVisible( true );
          }
          scroll.setVisible( true );
          timeTB.setVisible( true );
          pack();

        } else {
          checkDecimatedTrails();
          setPreferredSize( getSize() );

          waveView.setVisible( false );
          channelHeaderPanel.setVisible( false );
          markAxis.setVisible( false );
          markAxisHeader.setVisible( false );
          scroll.setVisible( false );
          timeTB.setVisible( false );
          actionZoomAllOut.perform();

          final int h = d.height - (waveView.getHeight() + scroll.getHeight() +
             (viewMarkers ? markAxis.getHeight() : 0));
          setSize( new Dimension( d.width - timeTB.getWidth(), h ));
          cbr.setMinimumHeight( h );
          cbr.setMaximumHeight( h );
          cbr.add( getWindow() );
        }
      }
    });
    topPane.add( ggTreeExp );
   
    gp.setGradientShift( 0, topPane.getPreferredSize().height );
   
    cp.add( topPane, BorderLayout.NORTH );
    cp.add( ggTrackPanel, BorderLayout.CENTER );
    cp.add( box, BorderLayout.SOUTH );
   
    // --- Tools ---
   
    pointerTool = new TimelinePointerTool();
    tools.put( new Integer( ToolAction.POINTER ), pointerTool );
    tools.put( new Integer( ToolAction.ZOOM ), new TimelineZoomTool() );

    // ---- TopPainter ----

    trackPainter  = new TopPainter() {
      public void paintOnTop( Graphics2D g2 )
      {
        Rectangle r;

        r = new Rectangle( 0, 0, wavePanel.getWidth(), wavePanel.getHeight() ); // getViewRect();
        if( !vpRecentRect.equals( r )) {
          recalcTransforms( r );
        }

        for( int i = 0; i < vpSelections.size(); i++ ) {
          r = (Rectangle) vpSelections.get( i );
          g2.setColor( (Color) vpSelectionColors.get( i ));
          g2.fillRect( vpSelectionRect.x, r.y - vpRecentRect.y, vpSelectionRect.width, r.height );
        }
       
        if( markVisible ) {
          markAxis.paintFlagSticks( g2, vpRecentRect );
        }
       
        g2.setColor( colrPosition );
        g2.drawLine( vpPosition, 0, vpPosition, vpRecentRect.height );

        if( vpZoomRect != null ) {
          g2.setColor( colrZoom );
          g2.setStroke( vpZoomStroke[ vpZoomStrokeIdx ]);
          g2.drawRect( vpZoomRect.x, vpZoomRect.y, vpZoomRect.width, vpZoomRect.height );
        }
      }
    };
    wavePanel.addTopPainter( trackPainter );

    // ---- listeners ----

    doc.timeline.addTimelineListener( this );
//    doc.addListener( this );
   
//    checkDecimatedTrails();

    doc.audioTracks.addListener( new SessionCollection.Listener() {
      public void sessionCollectionChanged( SessionCollection.Event e )
      {
        documentUpdate();
      }

      public void sessionObjectMapChanged( SessionCollection.Event e ) { /* ignored */ }

      public void sessionObjectChanged( SessionCollection.Event e )
      {
        // nothing
      }
    });
   
    doc.selectedTracks.addListener( new SessionCollection.Listener() {
      public void sessionCollectionChanged( SessionCollection.Event e )
      {
        updateSelectionAndRepaint();
      }

      public void sessionObjectMapChanged( SessionCollection.Event e ) { /* ignore */ }
      public void sessionObjectChanged( SessionCollection.Event e ) { /* ignore */ }
    });
 
    transport.addTransportListener( this );

    doc.markers.addListener( new Trail.Listener() {
      public void trailModified( Trail.Event e )
      {
        repaintMarkers( e.getAffectedSpan() );
      }
    });
   
    doc.getAudioTrail().addListener( new Trail.Listener() {
      public void trailModified( Trail.Event e )
      {
        if( !waveExpanded || !e.getAffectedSpan().touches( timelineVis )) return;
     
        updateOverviews( false, false );
      }
    });

    winListener = new AbstractWindow.Adapter() {
      public void windowClosing( AbstractWindow.Event e ) {
        actionClose.perform();
      }

      public void windowActivated( AbstractWindow.Event e )
      {
        // need to check 'disposed' to avoid runtime exception in doc handler if document was just closed
        if( !disposed ) {
          app.getDocumentHandler().setActiveDocument( DocumentFrame.this, doc );
          ((BasicWindowHandler) app.getWindowHandler()).setMenuBarBorrower( DocumentFrame.this );
        }
      }
    };
    this.addListener( winListener );

    waveView.addComponentListener( new ComponentAdapter() {
      public void componentResized( ComponentEvent e )
      {
        updateSelectionAndRepaint();
      }
    });
   
    timeTB.addToolActionListener( this );
    timeTB.selectTool( ToolAction.POINTER );
   
    playTimer = new Timer( 33, new ActionListener() {
      public void actionPerformed( ActionEvent e )
      {
        timelinePos = transport.getCurrentFrame();
        updatePositionAndRepaint();
        scroll.setPosition( timelinePos, 50, TimelineScroll.TYPE_TRANSPORT );
      }
    });
   
    // --- Actions ---
    actionNewFromSel  = new ActionNewFromSel();
    actionClose      = new ActionClose();
    actionSave      = new ActionSave();
    actionSaveAs    = new ActionSaveAs( false, false );
    actionSaveCopyAs  = new ActionSaveAs( true, false );
    actionSaveSelectionAs = new ActionSaveAs( true, true );
    actionSelectAll    = new ActionSelectAll();
    actionInsertRec    = new ActionInsertRec();

    actionProcess    = new ActionProcess();
    actionProcessAgain  = new ActionProcessAgain();
    actionFadeIn    = new ActionPlugIn( plugInPackage + "FadeIn" );
    actionFadeOut    = new ActionPlugIn( plugInPackage + "FadeOut" );
    actionGain      = new ActionPlugIn( plugInPackage + "Gain" );
    actionInvert    = new ActionPlugIn( plugInPackage + "Invert" );
    actionReverse    = new ActionPlugIn( plugInPackage + "Reverse" );
    actionRotateChannels = new ActionPlugIn( plugInPackage + "RotateChannels" );
    actionFScNeedlehole  = new ActionPlugIn( fscapePackage + "Needlehole" );

    actionDebugDump    = new ActionDebugDump();
    actionDebugVerify  = new ActionDebugVerify();

    actionIncVertMax  = new ActionVerticalMax( 2.0f, 6f );
    actionDecVertMax  = new ActionVerticalMax( 0.5f, -6f );
    actionIncVertMin  = new ActionVerticalMin( 6f );
    actionDecVertMin  = new ActionVerticalMin( -6f );
    actionIncHoriz    = new ActionSpanWidth( 2.0f );
    actionDecHoriz    = new ActionSpanWidth( 0.5f );
    actionZoomAllOut  = new ActionScroll( SCROLL_ENTIRE_SESSION );

    actionShowWindow  = new ShowWindowAction( this );

    imap.put( KeyStroke.getKeyStroke( KeyEvent.VK_DOWN, InputEvent.CTRL_MASK ), "incvmax" );
    amap.put( "incvmax", actionIncVertMax );
    imap.put( KeyStroke.getKeyStroke( KeyEvent.VK_UP, InputEvent.CTRL_MASK ), "decvmax" );
    amap.put( "decvmax", actionDecVertMax );
    imap.put( KeyStroke.getKeyStroke( KeyEvent.VK_DOWN, InputEvent.CTRL_MASK | InputEvent.ALT_MASK ), "incvmin" );
    amap.put( "incvmin", actionIncVertMin );
    imap.put( KeyStroke.getKeyStroke( KeyEvent.VK_UP, InputEvent.CTRL_MASK | InputEvent.ALT_MASK ), "decvmin" );
    amap.put( "decvmin", actionDecVertMin );
    imap.put( KeyStroke.getKeyStroke( KeyEvent.VK_LEFT, InputEvent.CTRL_MASK ), "inch" );
    imap.put( KeyStroke.getKeyStroke( KeyEvent.VK_OPEN_BRACKET, BasicMenuFactory.MENU_SHORTCUT ), "inch" );
    amap.put( "inch", actionIncHoriz );
    imap.put( KeyStroke.getKeyStroke( KeyEvent.VK_RIGHT, InputEvent.CTRL_MASK ), "dech" );
    imap.put( KeyStroke.getKeyStroke( KeyEvent.VK_CLOSE_BRACKET, BasicMenuFactory.MENU_SHORTCUT ), "dech" );
    amap.put( "dech", actionDecHoriz );
    imap.put( KeyStroke.getKeyStroke( KeyEvent.VK_RIGHT, myMeta ), "samplvl" );
    amap.put( "samplvl", new ActionSpanWidth( 0.0f ));
    imap.put( KeyStroke.getKeyStroke( KeyEvent.VK_ENTER, 0 ), "retn" );
    amap.put( "retn", new ActionScroll( SCROLL_SESSION_START ));
    imap.put( KeyStroke.getKeyStroke( KeyEvent.VK_LEFT, 0 ), "left" );
    amap.put( "left", new ActionScroll( SCROLL_SELECTION_START ));
    imap.put( KeyStroke.getKeyStroke( KeyEvent.VK_RIGHT, 0 ), "right" );
    amap.put( "right", new ActionScroll( SCROLL_SELECTION_STOP ));
    imap.put( KeyStroke.getKeyStroke( KeyEvent.VK_F, InputEvent.ALT_MASK ), "fit" );
    amap.put( "fit", new ActionScroll( SCROLL_FIT_TO_SELECTION ));
    imap.put( KeyStroke.getKeyStroke( KeyEvent.VK_A, InputEvent.ALT_MASK ), "entire" );
    imap.put( KeyStroke.getKeyStroke( KeyEvent.VK_LEFT, myMeta ), "entire" );
    amap.put( "entire", actionZoomAllOut );
    imap.put( KeyStroke.getKeyStroke( KeyEvent.VK_ENTER, InputEvent.SHIFT_MASK ), "seltobeg" );
    amap.put( "seltobeg", new ActionSelect( SELECT_TO_SESSION_START ));
    imap.put( KeyStroke.getKeyStroke( KeyEvent.VK_ENTER, InputEvent.SHIFT_MASK + InputEvent.ALT_MASK ), "seltoend" );
    amap.put( "seltoend", new ActionSelect( SELECT_TO_SESSION_END ));
    imap.put( KeyStroke.getKeyStroke( KeyEvent.VK_UP, 0 ), "postoselbegc" );
    amap.put( "postoselbegc", doc.timeline.getPosToSelAction( true, true ));
    imap.put( KeyStroke.getKeyStroke( KeyEvent.VK_DOWN, 0 ), "postoselendc" );
    amap.put( "postoselendc", doc.timeline.getPosToSelAction( false, true ));
    imap.put( KeyStroke.getKeyStroke( KeyEvent.VK_UP, InputEvent.ALT_MASK ), "postoselbeg" );
    amap.put( "postoselbeg", doc.timeline.getPosToSelAction( true, false ));
    imap.put( KeyStroke.getKeyStroke( KeyEvent.VK_DOWN, InputEvent.ALT_MASK ), "postoselend" );
    amap.put( "postoselend", doc.timeline.getPosToSelAction( false, false ));
    imap.put( KeyStroke.getKeyStroke( KeyEvent.VK_M, 0 ), "dropmark" );
    amap.put( "dropmark", new ActionDropMarker() );
    imap.put( KeyStroke.getKeyStroke( KeyEvent.VK_TAB , 0 ), "selnextreg" );
        imap.put( KeyStroke.getKeyStroke( KeyEvent.VK_LESS, 0 ), "selnextreg" );
    amap.put( "selnextreg", new ActionSelectRegion( SELECT_NEXT_REGION ));
        imap.put( KeyStroke.getKeyStroke( KeyEvent.VK_TAB , InputEvent.ALT_MASK ), "selprevreg" );
    imap.put( KeyStroke.getKeyStroke( KeyEvent.VK_LESS, InputEvent.ALT_MASK ), "selprevreg" );
    amap.put( "selprevreg", new ActionSelectRegion( SELECT_PREV_REGION ));
        imap.put( KeyStroke.getKeyStroke( KeyEvent.VK_TAB , InputEvent.SHIFT_MASK ), "extnextreg" );
    imap.put( KeyStroke.getKeyStroke( KeyEvent.VK_LESS, InputEvent.SHIFT_MASK ), "extnextreg" );
    amap.put( "extnextreg", new ActionSelectRegion( EXTEND_NEXT_REGION ));
        imap.put( KeyStroke.getKeyStroke( KeyEvent.VK_TAB , InputEvent.ALT_MASK + InputEvent.SHIFT_MASK ), "extprevreg" );
    imap.put( KeyStroke.getKeyStroke( KeyEvent.VK_LESS, InputEvent.ALT_MASK + InputEvent.SHIFT_MASK ), "extprevreg" );
    amap.put( "extprevreg", new ActionSelectRegion( EXTEND_PREV_REGION ));

    setFocusTraversalKeysEnabled( false ); // we want the tab! we gotta have that tab! ouwe!

    setDefaultCloseOperation( WindowConstants.DO_NOTHING_ON_CLOSE );
       
    // ---- menus and actions ----
    mr = app.getMenuBarRoot();
   
    mr.putMimic( "file.new.fromSelection", this, actionNewFromSel );
    mr.putMimic( "file.close", this, actionClose );
    mr.putMimic( "file.save", this, actionSave );
    mr.putMimic( "file.saveAs", this, actionSaveAs );
    mr.putMimic( "file.saveCopyAs", this, actionSaveCopyAs );
    mr.putMimic( "file.saveSelectionAs", this, actionSaveSelectionAs );

    mr.putMimic( "edit.undo", this, doc.getUndoManager().getUndoAction() );
    mr.putMimic( "edit.redo", this, doc.getUndoManager().getRedoAction() );
    mr.putMimic( "edit.cut", this, doc.getCutAction() );
    mr.putMimic( "edit.copy", this, doc.getCopyAction() );
    mr.putMimic( "edit.paste", this, doc.getPasteAction() );
    mr.putMimic( "edit.clear", this, doc.getDeleteAction() );
    mr.putMimic( "edit.selectAll", this, actionSelectAll );

    mr.putMimic( "timeline.insertSilence", this, doc.getSilenceAction() );
    mr.putMimic( "timeline.insertRecording", this, actionInsertRec );
    mr.putMimic( "timeline.trimToSelection", this, doc.getTrimAction() );

    mr.putMimic( "process.again", this, actionProcessAgain );
    mr.putMimic( "process.fadeIn", this, actionFadeIn );
    mr.putMimic( "process.fadeOut", this, actionFadeOut );
    mr.putMimic( "process.gain", this, actionGain );
    mr.putMimic( "process.invert", this, actionInvert );
    mr.putMimic( "process.reverse", this, actionReverse );
    mr.putMimic( "process.rotateChannels", this, actionRotateChannels );
    mr.putMimic( "process.fscape.needlehole", this, actionFScNeedlehole );

    mr.putMimic( "debug.dumpRegions", this, actionDebugDump );
    mr.putMimic( "debug.verifyRegions", this, actionDebugVerify );
   
    updateEditEnabled( false );

    AbstractWindowHandler.setDeepFont( cp, Collections.singletonList( timeTB ));
    GUIUtil.setDeepFont( timeTB, app.getGraphicsHandler().getFont( GraphicsHandler.FONT_SYSTEM | GraphicsHandler.FONT_MINI ));
    app.getMenuFactory().addToWindowMenu( actionShowWindow )// MUST BE BEFORE INIT()!!

    init();
    updateTitle();
    documentUpdate();

    addDynamicListening( new DynamicPrefChangeManager( app.getUserPrefs(), new String[] {
      PrefsUtil.KEY_VIEWNULLLINIE, PrefsUtil.KEY_VIEWVERTICALRULERS, PrefsUtil.KEY_VIEWMARKERS,
      PrefsUtil.KEY_TIMEUNITS, PrefsUtil.KEY_VERTSCALE, PrefsUtil.KEY_VIEWCHANMETERS },
      this ));

    initBounds()// be sure this is after documentUpdate!

    setVisible( true );
    toFront();
  }
 
    public void setLoop( boolean onOff ) {
        transTB.setLoop( onOff );
    }

  protected boolean alwaysPackSize()
  {
    return false;
  }
 
  /*
   */
  private void initBounds()
  {
    final Preferences      cp  = getClassPrefs();
    final BasicWindowHandler  bwh  = getWindowHandler();
    final Rectangle        sr  = bwh.getWindowSpace();
    final Dimension        dt  = stringToDimension( cp.get( KEY_TRACKSIZE, null ));
    final Dimension        d  = dt == null ? new Dimension() : dt;
    final float          hf  = (float) Math.sqrt( Math.max( 1, waveView.getNumChannels() ));
    final Dimension        winSize;
    final Rectangle        wr;
    int              w  = d.width;
    int              h  = d.height;
    sr.x    += 36;
    sr.y    += 36;
    sr.width  -= 60;
    sr.height  -= 60;
    if( w <= 0 ) {
      w = sr.width*2/3 - AudioTrackRowHeader.ROW_WIDTH;
    }
    if( h <= 0 ) {
      h = (sr.height - 106) / 4; // 106 = approx. extra space for title bar, tool bar etc.
    }
//System.out.println( "read KEY_TRACKSIZE : " + d );
//System.out.println( "w " + w + "; h " + h + "; hf " + hf );
    waveView.setPreferredSize( new Dimension( w, (int) (h * hf + 0.5f) ));
    pack();
    winSize = getSize();
    wr = new Rectangle( lastLeftTop.x + 21, lastLeftTop.y + 23,
        winSize.width, winSize.height );
    GUIUtil.wrapWindowBounds( wr, sr );
    lastLeftTop.setLocation( wr.getLocation() );
    setBounds( wr );
//System.out.println( "winSize " + winSize + "; wr " + wr );
    waveView.addComponentListener( new ComponentAdapter() {
      public void componentResized( ComponentEvent e )
      {
        if( waveExpanded ) {
          final Dimension dNew = e.getComponent().getSize();
          dNew.height = (int) (dNew.height / hf + 0.5f);
          if( !dNew.equals( d )) {
//System.out.println( "write KEY_TRACKSIZE : " + dNew );
            d.setSize( dNew );
            cp.put( KEY_TRACKSIZE, AppWindow.dimensionToString( dNew ));
          }
        }
      }
    });
  }
 
  protected void checkDecimatedTrails()
  {
    final DecimatedTrail dt;
   
    if( waveExpanded ) {
      if( verticalScale == PrefsUtil.VSCALE_FREQ_SPECT ) {
        if( doc.getDecimatedSonaTrail() == null ) {
          try {
            final DecimatedSonaTrail dst = doc.createDecimatedSonaTrail();
            // set initial freq bounds of waveview
            waveView.setFreqMinMax( dst.getMinFreq(), dst.getMaxFreq() );
          }
          catch( IOException e1 ) {
            e1.printStackTrace();
          }
        }
        dt = doc.getDecimatedSonaTrail();
      } else {
        if( doc.getDecimatedWaveTrail() == null ) {
          try {
            doc.createDecimatedWaveTrail();
          }
          catch( IOException e1 ) {
            e1.printStackTrace();
          }
        }
        dt = doc.getDecimatedWaveTrail();
      }
      if( dt != asyncTrail ) {
        if( asyncTrail != null ) asyncTrail.removeAsyncListener( this );
        asyncTrail = dt;
        if( asyncTrail != null ) asyncTrail.addAsyncListener( this );
      }
    }
  }
 
  public void addCatchBypass() { scroll.addCatchBypass(); }
  public void removeCatchBypass() { scroll.removeCatchBypass(); }

  public void repaintMarkers( Span affectedSpan )
  {
    if( !markVisible || !affectedSpan.touches( timelineVis )) return;
 
    final Span span   = affectedSpan.shift( -timelineVis.start );
    final Rectangle updateRect = new Rectangle(
      (int) (span.start * vpScale), 0,
      (int) (span.getLength() * vpScale) + 2, wavePanel.getHeight() ).
        intersection( new Rectangle( 0, 0, wavePanel.getWidth(), wavePanel.getHeight() ));
    if( !updateRect.isEmpty() ) {
      // update markAxis in any case, even if it's invisible
      // coz otherwise the flag stakes are not updated!
      wavePanel.update( markAxis );
      wavePanel.repaint( updateRect );
    }
  }

  public void playerCreated( SuperColliderPlayer p )
  {
    lmm.setInputs( p.getInputBus() );
    lmm.addTaskSync( p.getInputSync() );
  }

  public void playerDestroyed( SuperColliderPlayer p )
  {
    lmm.clearInputs();
  }

//  protected boolean alwaysPackSize()
//  {
//    return false;
//  }

  public void setSRCEnabled( boolean onOff )
  {
    lbSRC.setForeground( onOff ? Color.red : colrClear );
  }

  public void setForceMeters( boolean onOff )
  {
    if( onOff != forceMeters ) {
      forceMeters = onOff;
      showHideMeters();
      final int holdDur = forceMeters ? -1 : PeakMeter.DEFAULT_HOLD_DUR;
      for( int i = 0; i < channelMeters.length; i++ ) {
        channelMeters[ i ].setHoldDuration( holdDur );
      }
//      if( !forceMeters && !transport.isRunning() ) {
//        timeMetersPause = System.currentTimeMillis() + 5000;
//      }
    }
  }
 
  public float getMaxMeterHold()
  {
    float hold = Float.NEGATIVE_INFINITY;
    for( int i = 0; i < channelMeters.length; i++ ) {
      hold = Math.max( hold, channelMeters[ i ].getHoldDecibels() );
    }
    return hold;
  }
 
  public void clearMeterHold()
  {
    for( int i = 0; i < channelMeters.length; i++ ) {
      channelMeters[ i ].clearHold();
    }
  }
 
  private void showHideMeters()
  {
    final boolean visible  = chanMeters || forceMeters;

    if( metersPanel.isVisible() != visible ) {
      metersPanel.setVisible( visible );
//      if( visible) metersPanel.revalidate();
//      if( visible) {
//        metersPanel.setSize( metersPanel.getPreferredSize().width, waveHeaderPanel.getHeight() ); // pack();
//        for( int i = 0; i < channelMeters.length; i++ ) {
//          channelMeters[ i ].setSize( channelMeters[ i ].getPreferredSize().width, waveHeaderPanel.getHeight() / channelMeters.length );
//          channelMeters[ i ].setVisible( true );
//        }
//        metersPanel.revalidate();
//      }
//System.err.println( "metersPanel.getComponentCount() = "+metersPanel.getComponentCount()+"; size = "+metersPanel.getWidth()+", "+metersPanel.getHeight() );
//      waveHeaderView.makeCompactGrid();
//      startStopMeters();
    }
  }
 
  /**
   *  Recreates the main frame's title bar
   *  after a sessions name changed (clear/load/save as session)
   */
  public void updateTitle()
  {
//    final File        fDisp  = doc.getDisplayDescr().file;
    final AudioFileDescr[]  afds  = doc.getDescr();
    final String      name;
    final Icon        icn;
        final File              f;

    writeProtected  = false;

        f = afds.length == 0 ? null : afds[0].file;
        actionRevealFile.setFile(f);

        if( doc.getName() == null ) {
      name      = getResourceString( "frameUntitled" );
    } else {
      name      = doc.getName();
      try {
        for( int i = 0; i < afds.length; i++ ) {
          final File f1 = afds[ i ].file;
          if( f1 == null ) continue;
          writeProtected |= !f1.canWrite() || ((f1.getParentFile() != null) && !f1.getParentFile().canWrite());
        }
      } catch( SecurityException e ) { /* ignored */ }
    }

    if( writeProtected ) {
      icn = GUIUtil.getNoWriteIcon();
      if( lbWriteProtected.getIcon() != icn ) {
        lbWriteProtected.setIcon( icn );
      }
    } else if( lbWriteProtected.getIcon() != null ) {
      lbWriteProtected.setIcon( null );
    }
//    icnWriteProtected.setID( writeProtected ? MutableIcon.WRITE_PROTECTED : MutableIcon.INVISIBLE );
//    lbWriteProtected.repaint();
   
    if( internalFrames ) {
      if( doc.isDirty() ) {
        setTitle( "\u2022" + name );
      } else {
        setTitle( name );
      }
    } else {
      setTitle( app.getName() + (doc.isDirty() ? " - \u2022" : " - " ) + name );
            final Component c = getComponent();
            if (c instanceof JFrame) {
                final JFrame jf = (JFrame) c;
                jf.getRootPane().putClientProperty("Window.documentFile", f);
            }
    }
    actionShowWindow.putValue( Action.NAME, name );
    actionSave.setEnabled( !writeProtected && doc.isDirty() );
    setDirty( doc.isDirty() );
   
    final AudioFileInfoPalette infoBox = (AudioFileInfoPalette) app.getComponent( Main.COMP_AUDIOINFO );
    if( infoBox != null ) infoBox.updateDocumentName( doc );
   
    if( writeProtected && !wpHaveWarned && doc.isDirty() ) {
//      MutableIcon warnIcon = new MutableIcon( 128 );
//      warnIcon.setID( MutableIcon.WRITE_PROTECTED );
      final JOptionPane op = new JOptionPane( getResourceString( "warnWriteProtected" ), JOptionPane.WARNING_MESSAGE );
//      JOptionPane.showMessageDialog( getWindow(), getResourceString( "warnWriteProtected" ),
//        getResourceString( "msgDlgWarn" ), JOptionPane.WARNING_MESSAGE, null );
      BasicWindowHandler.showDialog( op, getWindow(), getResourceString( "msgDlgWarn" ));
      wpHaveWarned = true;
    }
  }
 
  // sync: attempts exclusive on MTE and shared on TIME!
  protected void updateOverviews( boolean justBecauseOfResize, boolean allTracks )
  {
//System.err.println( "update" );
    waveView.update( timelineVis );
    if( allTracks ) wavePanel.updateAll();
  }

  protected String getResourceString( String key )
  {
    return app.getResourceString( key );
  }

//  private BlendContext createBlendContext( long maxLength )
//  {
//    return BlendingAction.createBlendContext(
//      AbstractApplication.getApplication().getUserPrefs().node( BlendingAction.DEFAULT_NODE ),
//      timelineRate, maxLength );
//  }

  protected void documentUpdate()
  {
    final List        collChannelMeters;
    PeakMeter[]        meters;
    AudioTrackRowHeader    chanHead;
    AudioTrack        t;
    int            oldChannels, newChannels;
    Axis          chanRuler;
    PeakMeter        chanMeter;

    newChannels = doc.getDisplayDescr().channels;
    oldChannels  = collChannelHeaders.size();

    meters        = channelMeters;
    collChannelMeters  = new ArrayList( meters.length );
    for( int ch = 0; ch < meters.length; ch++ ) {
      collChannelMeters.add( meters[ ch ]);
    }
 
    // first kick out editors whose tracks have been removed
    for( int ch = 0; ch < oldChannels; ch++ ) {
      chanHead  = (AudioTrackRowHeader) collChannelHeaders.get( ch );
      t      = (AudioTrack) chanHead.getTrack();
      if( !doc.audioTracks.contains( t )) {
        chanHead  = (AudioTrackRowHeader) collChannelHeaders.remove( ch );
        chanMeter  = (PeakMeter) collChannelMeters.remove( ch );
        chanRuler  = (Axis) collChannelRulers.remove( ch );
        oldChannels--;
        // XXX : dispose trnsEdit (e.g. free vectors, remove listeners!!)
        flagsPanel.remove( chanHead );
        metersPanel.remove( chanMeter );
        rulersPanel.remove( chanRuler );
        ch--;
        chanHead.dispose();
        chanMeter.dispose();
        chanRuler.dispose();
      }
    }
    // next look for newly added transmitters and create editors for them
newLp:  for( int ch = 0; ch < newChannels; ch++ ) {
      t      = (AudioTrack) doc.audioTracks.get( ch );
      for( int ch2 = 0; ch2 < oldChannels; ch2++ ) {
        chanHead = (AudioTrackRowHeader) collChannelHeaders.get( ch );
        if( chanHead.getTrack() == t ) continue newLp;
      }
     
      chanHead = new AudioTrackRowHeader( t, doc.tracks, doc.selectedTracks, doc.getUndoManager() );
      collChannelHeaders.add( chanHead );
      flagsPanel.add( chanHead, ch );

      chanMeter = new PeakMeter();
      collChannelMeters.add( chanMeter );
      metersPanel.add( chanMeter, ch );

      chanRuler = new Axis( Axis.VERTICAL, Axis.FIXEDBOUNDS );
      collChannelRulers.add( chanRuler );
      rulersPanel.add( chanRuler, ch );
    }
   
    meters  = new PeakMeter[ collChannelMeters.size() ];
    for( int ch = 0; ch < meters.length; ch++ ) {
      meters[ ch ] = (PeakMeter) collChannelMeters.get( ch );
    }
    channelMeters  = meters;
    lmm.setView( new PeakMeterGroup( meters ));

    updateOverviews( false, true );
  }

//  private void initStrip( Axis chanRuler, PeakMeter chanMeter )
//  {
//    final Preferences prefs = app.getUserPrefs();
// 
//    chanMeter.setVisible( prefs.getBoolean( PrefsUtil.KEY_VIEWCHANMETERS, false ));
//    chanRuler.setVisible( prefs.getBoolean( PrefsUtil.KEY_VIEWVERTICALRULERS, false ));
//  }
 
  public ProcessingThread closeDocument( boolean force, Flag wasClosed )
  {
    doc.getTransport().stop();
    if( !force ) {
      final String name = getResourceString( "menuClose" );
      if( !confirmCancel( name )) {
        wasClosed.set( false );
        return null;
      }
      final ProcessingThread pt = confirmUnsaved( name, wasClosed );
      if( pt != null ) {
        pt.addListener( new ProcessingThread.Listener() {
          public void processStarted( ProcessingThread.Event e ) { /* ignored */ }
          public void processStopped( ProcessingThread.Event e )
          {
            if( e.isDone() ) {
              documentClosed();
            }
          }
        });
        return pt;
      }
    }
    if( wasClosed.isSet() ) {
      documentClosed();
    }
    return null;
  }
 
  protected void documentClosed()
  {
    disposed = true// important to avoid "too late window messages" to be processed; fucking swing doesn't kill them despite listener being removed
//System.err.println( "DocumentFrame.documentClosed()" );
    this.removeListener( winListener );
    actionShowWindow.dispose();
//    this.removeWindowFocusListener( winListener );  // otherwise we'll try to set an obsolete active doc
    app.getDocumentHandler().removeDocument( this, doc )// invokes doc.dispose() and hence this.dispose()
//    actionShowWindow.removeAll();
  }
 
  public void dispose()
  {
    playTimer.stop();

    app.getMenuFactory().removeFromWindowMenu( actionShowWindow );

    AudioTrackRowHeader  chanHead;
    Axis        chanRuler;
 
    lmm.dispose();
    wavePanel.dispose();
    while( !collChannelHeaders.isEmpty() ) {
      chanHead = (AudioTrackRowHeader) collChannelHeaders.remove( 0 );
      chanHead.dispose();
    }
    while( !collChannelRulers.isEmpty() ) {
      chanRuler = (Axis) collChannelRulers.remove( 0 );
      chanRuler.dispose();
    }
    for( int ch = 0; ch < channelMeters.length; ch++ ) {
      channelMeters[ ch ].dispose();
    }
    channelMeters = new PeakMeter[ 0 ];
    markAxis.stopListening();
    markAxis.dispose();
    timeAxis.dispose();
    timeTB.dispose();
    transTB.dispose();
   
    super.dispose();
  }

  private void updateEditEnabled( boolean enabled )
  {
    Action ma;
    ma      = doc.getCutAction();
    if( ma != null ) ma.setEnabled( enabled );
    ma      = doc.getCopyAction();
    if( ma != null ) ma.setEnabled( enabled );
    ma      = doc.getDeleteAction();
    if( ma != null ) ma.setEnabled( enabled );
    ma      = doc.getTrimAction();
    if( ma != null ) ma.setEnabled( enabled );
    actionProcess.setEnabled( enabled );
    actionNewFromSel.setEnabled( enabled );
    actionSaveSelectionAs.setEnabled( enabled );
  }

//  public ProcessingThread insertSilence( long pos, long numFrames )
//  {
//    return actionInsertSilence.initiate( pos, numFrames );
//  }

  /*
   *  Checks if there are unsaved changes to
   *  the session. If so, displays a confirmation
   *  dialog. Invokes Save/Save As depending
   *  on user selection. IF the doc was not dirty,
   *  or if &quot;Cancel&quot; or
   *  &quot;Don't save&quot; was chosen, the
   *  method returns <code>null</code> and the
   *  <code>confirmed</code> flag reflects whether
   *  the document should be closed. If a saving
   *  process should be started, that process is
   *  returned. Note that the <code>ProcessingThread</code>
   *  in this case has not yet been started, as to
   *  allow interested objects to install a listener
   *  first. So it's their job to call the <code>start</code>
   *  method!
   *
   *  @param  actionName    name of the action that
   *              threatens the session
   *  @param  confirmed    a flag that will be set to <code>true</code> if
   *              the doc is allowed to be closed
   *              (doc was not dirty or user chose &quot;Don't save&quot;),
   *              otherwise <code>false</code> (save process
   *              initiated or user chose &quot;Cancel&quot;).
   *  @return          a saving process yet to be started or <code>null</code>
   *              if the doc needn't/shouldn't be saved
   *
   *  @see  de.sciss.eisenkraut.util.ProcessingThread#start
   */
  private ProcessingThread confirmUnsaved( String actionName, Flag confirmed )
  {
    if( !doc.isDirty() ) {
      confirmed.set( true );
      return null;
    }
   
    final Object[]      options  = { getResourceString( "buttonSave" ),
                      getResourceString( "buttonCancel" ),
                      getResourceString( "buttonDontSave" )};
    final int        choice;
//    final AudioFileDescr  displayAFD  = doc.getDisplayDescr();
    final String      name;
    final JOptionPane    op;
    final JDialog       d;
    final JRootPane      rp;
    final Flag        dont    = new Flag( false );
    AudioFileDescr[]    afds    = doc.getDescr();
   
    if( doc.getName() == null ) {
      name = getResourceString( "frameUntitled" );
    } else {
      name = doc.getName();
    }
   
//    choice = JOptionPane.showOptionDialog( getWindow(), name + " :\n" + getResourceString( "optionDlgUnsaved" ),
//                         actionName, JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE, null,
//                         options, options[1] );
    op = new JOptionPane( name + " :\n" + getResourceString( "optionDlgUnsaved" ),
                          JOptionPane.WARNING_MESSAGE, JOptionPane.YES_NO_CANCEL_OPTION, null,
                          options, options[ 1 ]);
    d = op.createDialog( getWindow(), actionName );
    rp = d.getRootPane();
    if( rp != null ) {
      rp.getInputMap( JComponent.WHEN_IN_FOCUSED_WINDOW ).put(
        KeyStroke.getKeyStroke( KeyEvent.VK_D, BasicMenuFactory.MENU_SHORTCUT ), "dont" );
      rp.getActionMap().put( "dont", new AbstractAction() {
        public void actionPerformed( ActionEvent e )
        {
          dont.settrue );
          d.dispose();
        }
      });
    }
    BasicWindowHandler.showDialog( d );
//    d.setVisible( true );
    if( dont.isSet() ) {
      choice = 2;
    } else {
      final Object value = op.getValue();
      if( (value == null) || (value == options[ 1 ])) {
        choice = 1;
      } else if( value == options[ 0 ]) {
        choice = 0;
      } else if( value == options[ 2 ]) {
        choice = 2;
      } else {
        choice = -1// throws assertion error in switch block
      }
    }

    switch( choice ) {
    case JOptionPane.CLOSED_OPTION:
    case 1// cancel
      confirmed.set( false );
      return null;
     
    case 2// don't save
      confirmed.set( true );
      return null;
     
    case 0:
      confirmed.set( false );
      if( (doc.getDisplayDescr().file == null) || writeProtected ) {
        afds = actionSaveAs.query( afds );
      }
      if( afds != null ) {
        return actionSave.initiate( actionSave.getValue( Action.NAME ).toString(), null, afds, null, true, false, false );
      }
      return null;
     
    default:
      assert false : choice;
      return null;
    }
  }

  private boolean confirmCancel( String actionName )
  {
    if( doc.checkProcess( 50 )) {
      return true;
    }
   
    final int        choice;
//    final AudioFileDescr  displayAFD  = doc.getDisplayDescr();
    final String      name;
   
    if( doc.getName() == null ) {
      name = getResourceString( "frameUntitled" );
    } else {
      name = doc.getName();
    }
   
    final JOptionPane op = new JOptionPane( name + " :\n" + getResourceString( "optionDlgProcessing" ) +
                                            "\n(" + doc.getProcessName() + ")?", JOptionPane.WARNING_MESSAGE,
                                            JOptionPane.YES_NO_OPTION );
//    choice = JOptionPane.showConfirmDialog( getWindow(), name + " :\n" + getResourceString( "optionDlgProcessing" ) +
//                                            "\n(" + doc.getProcessName() + ")?",
//                          actionName, JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE );
    choice = BasicWindowHandler.showDialog( op, getWindow(), actionName );
   
    switch( choice ) {
    case JOptionPane.CLOSED_OPTION:
    case JOptionPane.NO_OPTION:
      return false;
     
    case JOptionPane.YES_OPTION:  // abort
      doc.cancelProcess( true );
      return true;
     
    default:
      assert false : choice;
      return false;
    }
  }

  /**
   *  Only call in the Swing thread!
   */
  protected void updatePositionAndRepaint()
  {
    boolean pEmpty, cEmpty;
    int    x, x2;
   
    pEmpty = (vpPositionRect.x + vpPositionRect.width < 0) || (vpPositionRect.x > vpRecentRect.width);
    if( !pEmpty ) vpUpdateRect.setBounds( vpPositionRect );

    if( vpScale > 0f ) {
      vpPosition  = (int) ((timelinePos - timelineVis.getStart()) * vpScale + 0.5f);
      // choose update rect such that even a paint manager delay of 200 milliseconds
      // will still catch the (then advanced) position so we don't see flickering!
      // XXX this should take playback rate into account, though
      vpPositionRect.setBounds( vpPosition, 0, Math.max( 1, (int) (vpScale * timelineRate * 0.2f) ), vpRecentRect.height );
    } else {
      vpPosition  = -1;
      vpPositionRect.setBounds( 0, 0, 0, 0 );
    }

    cEmpty = (vpPositionRect.x + vpPositionRect.width <= 0) || (vpPositionRect.x > vpRecentRect.width);
    if( pEmpty ) {
      if( cEmpty ) return;
      x   = Math.max( 0, vpPositionRect.x );
      x2  = Math.min( vpRecentRect.width, vpPositionRect.x + vpPositionRect.width );
      vpUpdateRect.setBounds( x, vpPositionRect.y, x2 - x, vpPositionRect.height );
    } else {
      if( cEmpty ) {
        x   = Math.max( 0, vpUpdateRect.x );
        x2  = Math.min( vpRecentRect.width, vpUpdateRect.x + vpUpdateRect.width );
        vpUpdateRect.setBounds( x, vpUpdateRect.y, x2 - x, vpUpdateRect.height );
      } else {
        x   = Math.max( 0, Math.min( vpUpdateRect.x, vpPositionRect.x ));
        x2  = Math.min( vpRecentRect.width, Math.max( vpUpdateRect.x + vpUpdateRect.width,
                              vpPositionRect.x + vpPositionRect.width ));
        vpUpdateRect.setBounds( x, vpUpdateRect.y, x2 - x, vpUpdateRect.height );
      }
    }
    if( !vpUpdateRect.isEmpty() ) {
      wavePanel.repaint( vpUpdateRect );
    }
  }

  /**
   *  Only call in the Swing thread!
   */
  protected void updateSelectionAndRepaint()
  {
    final Rectangle r = new Rectangle( 0, 0, wavePanel.getWidth(), wavePanel.getHeight() );
 
    vpUpdateRect.setBounds( vpSelectionRect );
    recalcTransforms( r );
    updateSelection();
    if( vpUpdateRect.isEmpty() ) {
      vpUpdateRect.setBounds( vpSelectionRect );
    } else if( !vpSelectionRect.isEmpty() ) {
      vpUpdateRect = vpUpdateRect.union( vpSelectionRect );
    }
    vpUpdateRect = vpUpdateRect.intersection( new Rectangle( 0, 0, wavePanel.getWidth(), wavePanel.getHeight() ));
    if( !vpUpdateRect.isEmpty() ) {
      wavePanel.repaint( vpUpdateRect );
    }
  }
 
  /**
   *  Only call in the Swing thread!
   */
  private void updateTransformsAndRepaint( boolean verticalSelection )
  {
    final Rectangle r = new Rectangle( 0, 0, wavePanel.getWidth(), wavePanel.getHeight() );

    vpUpdateRect = vpSelectionRect.union( vpPositionRect );
    recalcTransforms( r );
    if( verticalSelection ) updateSelection();
    vpUpdateRect = vpUpdateRect.union( vpPositionRect ).union( vpSelectionRect ).intersection( r );
    if( !vpUpdateRect.isEmpty() ) {
      wavePanel.repaint( vpUpdateRect )// XXX ??
    }
  }
 
  protected void recalcTransforms( Rectangle newRect )
  {
    int x, w;
   
    vpRecentRect = newRect; // getViewRect();
 
    if( !timelineVis.isEmpty() ) {
      vpScale      = (float) vpRecentRect.width / (float) timelineVis.getLength(); // - 1;
      playTimer.setDelay( Math.min( (int) (1000 / (vpScale * timelineRate * playRate)), 33 ));
      vpPosition    = (int) ((timelinePos - timelineVis.getStart()) * vpScale + 0.5f);
      vpPositionRect.setBounds( vpPosition, 0, 1, vpRecentRect.height );
      if( !timelineSel.isEmpty() ) {
        x      = (int) ((timelineSel.getStart() - timelineVis.getStart()) * vpScale + 0.5f) + vpRecentRect.x;
        w      = Math.max( 1, (int) ((timelineSel.getStop() - timelineVis.getStart()) * vpScale + 0.5f) - x );
        vpSelectionRect.setBounds( x, 0, w, vpRecentRect.height );
      } else {
        vpSelectionRect.setBounds( 0, 0, 0, 0 );
      }
    } else {
      vpScale      = 0.0f;
      vpPosition    = -1;
      vpPositionRect.setBounds( 0, 0, 0, 0 );
      vpSelectionRect.setBounds( 0, 0, 0, 0 );
    }
  }

  // sync: caller must sync on timeline + grp + tc
  private void updateSelection()
  {
    Rectangle  r;
    Track    t;
    int      x, y;

    vpSelections.clear();
    vpSelectionColors.clear();
    if( !timelineSel.isEmpty() ) {
      x      = waveView.getX();
      y      = waveView.getY();
      vpSelections.add( timeAxis.getBounds() );
      vpSelectionColors.add( colrSelection );
      t      = doc.markerTrack;
      vpSelections.add( markAxis.getBounds() );
      vpSelectionColors.add( doc.selectedTracks.contains( t ) ? colrSelection : colrSelection2 );
      for( int ch = 0; ch < waveView.getNumChannels(); ch++ ) {
        r    = new Rectangle( waveView.rectForChannel( ch ));
        r.translate( x, y );
        t    = (Track) doc.audioTracks.get( ch );
        vpSelections.add( r );
        vpSelectionColors.add( doc.selectedTracks.contains( t ) ? colrSelection : colrSelection2 );
      }
    }
  }

  protected void setZoomRect( Rectangle r )
  {
    vpZoomRect    = r;
    vpZoomStrokeIdx  = (vpZoomStrokeIdx + 1) % vpZoomStroke.length;

    wavePanel.repaint();
  }
 
// ------------- DecimatedTrail.AsyncListener interface -------------

  public void asyncFinished( DecimatedTrail.AsyncEvent e )
  {
    final DecimatedTrail dt = e.getDecimatedTrail();
    dt.removeAsyncListener( this );
    if( dt == asyncTrail ) asyncTrail = null;
    updateOverviews( false, true );
  }

  public void asyncUpdate( DecimatedTrail.AsyncEvent e )
  {
    updateOverviews( false, true );
  }
 
// ---------------- ProgressComponent interface ----------------

  public void addCancelListener( ActionListener l )
  {
    pProgress.addCancelListener( l );
  }

  public void removeCancelListener( ActionListener l )
  {
    pProgress.removeCancelListener( l );
  }

  public Component getComponent()
  {
    return getWindow();
  }
 
  public void resetProgression()
  {
    pProgress.resetProgression();
    pOverlay.performFade( 1f, 1000, 250 );
  }
 
  public void setProgression( float p )
  {
    pProgress.setProgression( p );
  }
 
  public void  finishProgression( int result )
  {
    if( result != CANCELLED ) pProgress.finishProgression( result );
    pOverlay.performFade( 0f, result == CANCELLED ? 0 : 4000, 250 );
  }
 
  public void setProgressionText( String text )
  {
    pProgress.setProgressionText( text );
  }
 
  public void showMessage( int type, String text )
  {
    pProgress.showMessage( type, text );
  }
 
  public void displayError( Exception e, String processName )
  {
    BasicWindowHandler.showErrorDialog( getWindow(), e, processName );
  }

  protected void updateAFDGadget()
  {
    final AudioFileDescr  displayAFD  = doc.getDisplayDescr();
    final AudioFileDescr[]  afds    = doc.getDescr();
    displayAFD.rate            = timelineRate;
    displayAFD.length          = timelineLen;
    for( int i = 0; i < afds.length; i++ ) {
      afds[ i ].rate          = displayAFD.rate;
      afds[ i ].length        = displayAFD.length;
    }
    ggAudioFileDescr.setText( displayAFD.getFormat() );
   
    pOverlay.performFade( 0f, 1000, 250 );
//    if( !ggAudioFileDescr.isVisible() ) {
//      hideProgressBarTimer.stop();
//      hideProgressBarListener.actionPerformed( null );
//    }
  }

  protected void updateCursorFormat()
  {
    final AudioFileDescr displayAFD  = doc.getDisplayDescr();
    csrInfoBits            = displayAFD.bitsPerSample;
    csrInfoIsInt          = displayAFD.sampleFormat == AudioFileDescr.FORMAT_INT;
  }

  protected void updateVerticalRuler()
  {
    final VectorSpace  spc;
    final float      min, max;
    Axis        chanRuler;
   
    switch( waveView.getVerticalScale() ) {
    case PrefsUtil.VSCALE_AMP_LIN:
      min = waveView.getAmpLinMin() * 100;
      max = waveView.getAmpLinMax() * 100;
      spc = VectorSpace.createLinSpace( 0.0, 1.0, min, max, null, null, null, null );
      break;
    case PrefsUtil.VSCALE_AMP_LOG:
      min = waveView.getAmpLogMin();
      max = waveView.getAmpLogMax();
      spc = VectorSpace.createLinSpace( 0.0, 1.0, min, max, null, null, null, null );
      break;
    case PrefsUtil.VSCALE_FREQ_SPECT:
      min = waveView.getFreqMin();
      max = waveView.getFreqMax();
      spc = VectorSpace.createLinLogSpace( 0.0, 1.0, min, max, Math.sqrt( min * max ), null, null, null, null );
      break;
    default:
      assert false : waveView.getVerticalScale();
      spc = null;
    }

    for( int i = 0; i < collChannelRulers.size(); i++ ) {
      chanRuler  = (Axis) collChannelRulers.get( i );
      chanRuler.setSpace( spc );
    }
  }

// ---------------- TimelineListener interface ----------------

  public void timelineSelected( TimelineEvent e )
    {
    final boolean  wasEmpty = timelineSel.isEmpty();
    final boolean  isEmpty;
 
    timelineSel  = doc.timeline.getSelectionSpan();

    updateSelectionAndRepaint();
    isEmpty  = timelineSel.isEmpty();
    if( wasEmpty != isEmpty ) {
      updateEditEnabled( !isEmpty );
    }
    }

  // warning : don't call doc.setAudioFileDescr, it will restore the old markers!
  public void timelineChanged( TimelineEvent e )
    {
    timelineRate        = doc.timeline.getRate();
    timelineLen          = doc.timeline.getLength();
    playTimer.setDelay( Math.min( (int) (1000 / (vpScale * timelineRate * playRate)), 33 ));
    updateAFDGadget();
    updateOverviews( false, true );
    }

  public void timelinePositioned( TimelineEvent e )
  {
    timelinePos = doc.timeline.getPosition();
   
    updatePositionAndRepaint();
    scroll.setPosition( timelinePos, 0, pointerTool.validDrag ?
      TimelineScroll.TYPE_DRAG : TimelineScroll.TYPE_UNKNOWN );
  }

    public void timelineScrolled( TimelineEvent e )
    {
//System.out.println( "scrolled " + doc.timeline.getVisibleSpan() );
      timelineVis  = doc.timeline.getVisibleSpan();

    updateOverviews( false, true );
    updateTransformsAndRepaint( false );
    }

// ---------------- TransportListener interface ----------------

  public void transportPlay( Transport t, long pos, double rate )
  {
    playRate = rate;
    playTimer.setDelay( Math.min( (int) (1000 / (vpScale * timelineRate * playRate)), 33 ));
    playTimer.restart();
  }
 
  public void transportStop( Transport t, long pos )
  {
    playTimer.stop();
  }

  public void transportPosition( Transport t, long pos, double rate ) { /* ignored */ }
  public void transportReadjust( Transport t, long pos, double rate ) { /* ignored */ }

  public void transportQuit( Transport t )
  {
    playTimer.stop();
  }

// ---------------- RealtimeConsumer interface ----------------

//  /**
//   *  Requests 30 fps notification (no data block requests).
//   *  This is used to update the timeline position during transport
//   *  playback.
//   */
//  public RealtimeConsumerRequest createRequest( RealtimeContext context )
//  {
//    RealtimeConsumerRequest request = new RealtimeConsumerRequest( this, context );
//    // 30 fps is visually fluent
//    request.notifyTickStep  = RealtimeConsumerRequest.approximateStep( context, 30 );
//    request.notifyTicks    = true;
//    request.notifyOffhand  = true;
//    return request;
//  }
// 
//  public void realtimeTick( RealtimeContext context, long timelinePos )
//  {
//    this.timelinePos = timelinePos;
//updatePositionAndRepaint();
//    scroll.setPosition( timelinePos, 50, TimelineScroll.TYPE_TRANSPORT );
//  }
//
//  public void offhandTick( RealtimeContext context, long timelinePos )
//  {
//  }

// ---------------- ToolListener interface ----------------
  // sync: attemptShared DOOR_TRNS
  public void toolChanged( ToolActionEvent e )
  {
    if( activeTool != null ) {
      activeTool.toolDismissed( waveView );
    }

    activeTool = (AbstractTool) tools.get( new Integer( e.getToolAction().getID() ));
    if( activeTool != null ) {
      waveView.setCursor( e.getToolAction().getDefaultCursor() );
      activeTool.toolAcquired( waveView );
    } else {
      waveView.setCursor( null );
    }
  }

// ---------------- PreferenceChangeListener interface ----------------

  public void preferenceChange( PreferenceChangeEvent e )
  {
    final String key = e.getKey();
       
    if( key == PrefsUtil.KEY_VIEWNULLLINIE ) {
      waveView.setNullLinie( e.getNode().getBoolean( e.getKey(), false ));
    } else if( key == PrefsUtil.KEY_VIEWVERTICALRULERS ) {
      final boolean visible = e.getNode().getBoolean( e.getKey(), false );
      rulersPanel.setVisible( visible );
    } else if( key == PrefsUtil.KEY_VIEWCHANMETERS ) {
      chanMeters = e.getNode().getBoolean( e.getKey(), false );
      showHideMeters();
    } else if( key == PrefsUtil.KEY_VIEWMARKERS ) {
      viewMarkers = e.getNode().getBoolean( e.getKey(), false );
      markVisible  = viewMarkers && waveExpanded;
      if( waveExpanded ) {
        markAxis.setVisible( markVisible );
        markAxisHeader.setVisible( markVisible );
        wavePanel.updateAll();
      }
      if( markVisible ) {
        markAxis.startListening();
      } else {
        markAxis.stopListening();
      }
    } else if( key == PrefsUtil.KEY_TIMEUNITS ) {
      final boolean timeSmps = e.getNode().getInt( key, PrefsUtil.TIME_SAMPLES ) == PrefsUtil.TIME_SAMPLES;
      msgCsr1.applyPattern( timeSmps ? smpPtrn : timePtrn );
    } else if( key == PrefsUtil.KEY_VERTSCALE) {
      verticalScale = e.getNode().getInt( key, PrefsUtil.VSCALE_AMP_LIN );
      checkDecimatedTrails(); // needs to be before setVert.scale / updateRuler!
      waveView.setVerticalScale( verticalScale );
      updateVerticalRuler();
    }
  }
 
// ---------------- ClipboardOwner interface ----------------

  public void lostOwnership( Clipboard clipboard, Transferable contents )
  {
    // XXX evtl. dispose() aufrufen
  }
 
// ---------------- internal action classes ----------------

  private class ActionDebugDump
  extends MenuAction
  {
    protected ActionDebugDump() { /* empty */ }

    public void actionPerformed( ActionEvent e )
    {
      System.err.println( "------------ Document: "+doc.getDisplayDescr().file+" ------------" );
      doc.getAudioTrail().debugDump();
      System.err.println( "   --------- decimated ---------" );
      doc.getDecimatedWaveTrail().debugDump();
    }
  }

  private class ActionDebugVerify
  extends MenuAction
  {
    protected ActionDebugVerify() { /* empty */ }

    public void actionPerformed( ActionEvent e )
    {
      System.err.println( "------------ Document: " + doc.getDisplayDescr().file + " ------------" );
      doc.getAudioTrail().debugVerifyContiguity();
      System.err.println("   --------- decimated ---------");
      doc.getDecimatedWaveTrail().debugVerifyContiguity();
    }
  }

  private class ActionNewFromSel
  extends MenuAction
  {
    protected ActionNewFromSel() { /* empty */ }

    public void actionPerformed( ActionEvent e )
    {
      final ClipboardTrackList  tl    = doc.getSelectionAsTrackList();
      final Session        doc2;
      final AudioFileDescr    afd, afd2;
      final int          selChans;
      final ProcessingThread    pt;
     
      if( tl == null ) return;
     
      selChans      = tl.getTrackNum( AudioTrail.class );
     
      afd          = doc.getDisplayDescr();
      afd2        = new AudioFileDescr();
      afd2.bitsPerSample  = afd.bitsPerSample;
      afd2.channels    = selChans;
      afd2.rate      = afd.rate;
      afd2.sampleFormat  = afd.sampleFormat;
     
      doc2        = ((MenuFactory) app.getMenuFactory()).newDocument( afd2 );
      if( doc2 == null ) {
        // it's important that the clipboard tl be diposed
        // when not used any more
        tl.dispose();
        return;
      }
     
      pt = doc2.pasteTrackList( tl, 0, getResourceString( "menuPaste" ), Session.EDIT_INSERT );
      if( pt != null ) {
        pt.addListener( new ProcessingThread.Listener() {
          public void processStarted( ProcessingThread.Event e1 ) { /* ignored */ }
          public void processStopped( ProcessingThread.Event e2 ) {
            tl.dispose();
          }
        });
        doc.start( pt );
      } else {
        tl.dispose();
      }
    }
  } // actionNewFromSelClass

  // action for the Save-Session menu item
  private class ActionClose
  extends MenuAction
  {
    protected ActionClose() { /* empty */ }

    public void actionPerformed( ActionEvent e )
    {
      perform();
    }
   
    public void perform()
    {
      final ProcessingThread pt = closeDocument( false, new Flag( false ));
      if( pt != null ) doc.start( pt );
    }
  }

  // action for the Save-Session menu item
  private class ActionSave
  extends MenuAction
  {
    protected ActionSave() { /* empty */ }

    /**
     *  Saves a Session. If the file
     *  wasn't saved before, a file chooser
     *  is shown before.
     */
    public void actionPerformed( ActionEvent e )
    {
      final AudioFileDescr  displayAFD  = doc.getDisplayDescr();     
      final AudioFileDescr[]  afds;

      if( displayAFD.file == null ) {
        afds = actionSaveAs.query( doc.getDescr() );
      } else {
        afds = doc.getDescr();
      }
      if( afds != null ) {
        perform( getValue( NAME ).toString(), afds );
      }
    }
   
    protected void perform( String name, AudioFileDescr[] afds )
    {
      perform( name, null, afds, null, true, false, false );
    }

    protected void perform( String name, Span span, AudioFileDescr[] afds,
                int[] channelMap, boolean saveMarkers, boolean asCopy, boolean openAfterSave )
    {
      final ProcessingThread pt = initiate( name, span, afds, channelMap, saveMarkers, asCopy, openAfterSave );
      if( pt != null ) doc.start( pt );
    }

    protected ProcessingThread initiate( String name, final Span span, final AudioFileDescr[] afds,
                       int[] channelMap, boolean saveMarkers, final boolean asCopy, final boolean openAfterSave )
    {
      final ProcessingThread pt = doc.procSave( name, span, afds, channelMap, saveMarkers, asCopy );
      if( pt == null ) return null;
     
      pt.addListener( new ProcessingThread.Listener() {
        public void processStopped( ProcessingThread.Event e )
        {
          if( !e.isDone() ) return;

          wpHaveWarned = false;

          if( !asCopy ) {
            if( afds.length == 1 ) app.getMenuFactory().addRecent( afds[ 0 ].file );
            updateAFDGadget();
            updateCursorFormat();
          }
          if( openAfterSave ) {
            if( afds.length == 1 ) {
              app.getMenuFactory().openDocument( afds[ 0 ].file );
            } else {
              final File[] fs = new File[ afds.length ];
              for( int i = 0; i < afds.length; i++ ) {
                fs[ i ] = afds[ i ].file;
              }
              ((MenuFactory) app.getMenuFactory()).openDocument( fs );
            }
          }
        }
       
        public void processStarted( ProcessingThread.Event e ) { /* ignored */ }
      });
      return pt;
    }
  }
 
  // action for the Save-Session-As menu item
  private class ActionSaveAs
  extends MenuAction
  {
    private final boolean  asCopy;
    private final boolean  selection;
    private final Flag    openAfterSave;
 
    protected ActionSaveAs( boolean asCopy, boolean selection )
    {
      if( selection && !asCopy ) throw new IllegalArgumentException();

      this.asCopy    = asCopy;
      this.selection  = selection;
      openAfterSave  = new Flag( false );
    }
 
    /*
     *  Query a file name from the user and save the Session
     */
    public void actionPerformed( ActionEvent e )
    {
      final List  infos    = Track.getInfos( doc.selectedTracks.getAll(), doc.tracks.getAll() );
      boolean    saveMarkers  = true;
      int[]     channelMap  = null;
      if( selection ) {
        for( int i = 0; i < infos.size(); i++ ) {
          Track.Info ti = (Track.Info) infos.get( i );
          if( ti.trail instanceof AudioTrail ) {
            int numSelChannels = 0;
            for( int j = 0; j < ti.trackMap.length; j++ ) {
              if( ti.trackMap[ j ]) numSelChannels++;
            }
            channelMap = new int[ numSelChannels ];
            for( int j = 0, k = 0; j < ti.trackMap.length; j++ ) {
              if( ti.trackMap[ j ]) channelMap[ k++ ] = j;
            }
          } else if( ti.trail instanceof MarkerTrail ) {
            saveMarkers = ti.selected;
          }
        }
      }
      final AudioFileDescr[] afds = query( doc.getDescr(), channelMap, saveMarkers, asCopy, selection, openAfterSave );
      if( afds != null ) {
        actionSave.perform( getValue( NAME ).toString(), selection ? timelineSel : null, afds, channelMap, saveMarkers, asCopy, openAfterSave.isSet() );
      }
    }
   
    protected AudioFileDescr[] query( AudioFileDescr[] protoType )
    {
      return query( protoType, null, true, false, false, null );
    }

    /**
     *  Open a file chooser so the user
     *  can select a new output file and format for the session.
     *
     *  @return the AudioFileDescr representing the chosen file path
     *      and format or <code>null</code>
     *      if the dialog was cancelled.
     *
     *  @todo  should warn user if saveMarkers is true and format does not support it
     */
    protected AudioFileDescr[] query( AudioFileDescr[] protoType, int[] channelMap, boolean saveMarkers,
                      boolean asCopySettings, boolean selectionSettings, Flag openAfterSaveSettings )
    {
      if( protoType.length == 0 ) return null;
   
//      final FileDialog      fDlg;
      final AudioFileDescr[]    afds;
      final AudioFileFormatPane  affp;
//      final JOptionPane      dlg;
      final SpringPanel      msgPane;
      final PathField[]      ggPathFields;
      final int[]          channelsUsed  = new int[ protoType.length ];
      final JCheckBox        ggOpenAfterSave;
      final String        prefsDirKey    = selectionSettings ? PrefsUtil.KEY_FILESAVESELDIR : PrefsUtil.KEY_FILESAVEDIR;
      final JPanel        p;
      int              filesUsed    = 0;
      File            f; // , f2;
      String[]          queryOptions  = { getResourceString( "buttonSave" ),
                                getResourceString( "buttonCancel" )};
      int              i, result;
      String            str;
      JLabel            lb;
      String            fileName, dirName;
      boolean            setFocus    = false;
      int              y        = 0;
     
//System.out.print( "channelMap = [ " );
//for( int kkk = 0; kkk < channelMap.length; kkk++ ) System.out.print( (kkk > 0 ? ", " : "") + channelMap[ kkk ]);
//System.out.println( " ]" );
     
      msgPane      = new SpringPanel( 4, 2, 4, 2 );
      ggPathFields  = new PathField[ protoType.length ];
      affp      = new AudioFileFormatPane( AudioFileFormatPane.FORMAT | AudioFileFormatPane.ENCODING );
      affp.fromDescr( protoType[0] );
      lb        = new JLabel( getResourceString( "labelOutputFile" ), RIGHT );
//      lb.setLabelFor( ggPathField );
      msgPane.gridAdd( lb, 0, y );
      for( int j = 0, chanOff = 0; j < protoType.length; chanOff += protoType[ j ].channels, j++, y++ ) {
        if( channelMap == null ) {
          channelsUsed[ j ] = protoType[ j ].channels;
        } else {
          for( int k = 0; k < channelMap.length; k++ ) {
            if( (channelMap[ k ] >= chanOff) && (channelMap[ k ] < chanOff + protoType[ j ].channels) ) {
              channelsUsed[ j ]++;
            }
          }
        }
//System.out.println( "channelsUsed[ " + j + " ] = " + channelsUsed[ j ]);
        if( channelsUsed[ j ] == 0 ) continue;

        filesUsed++;
        ggPathFields[ j ] = new PathField( PathField.TYPE_OUTPUTFILE, getValue( NAME ).toString() );
        if( protoType[ j ].file == null ) {
          fileName  = getResourceString( "frameUntitled" ) + (ggPathFields.length > 1 ? "-" + (j+1) : "");
        } else if( asCopySettings || selectionSettings ) {
          str  = protoType[ j ].file.getName();
          i  = str.lastIndexOf( '.' );
          if( i == -1 ) i = str.length();
          fileName = str.substring( 0, i ) + (selectionSettings ? getResourceString( "fileDlgCut" ) : " " + getResourceString( "fileDlgCopy" )); // suffix is appended by affp!
        } else {
          fileName = protoType[ j ].file.getName();
        }
        dirName = app.getUserPrefs().get( prefsDirKey, protoType[ j ].file == null ? System.getProperty( "user.home" ) : protoType[ j ].file.getParent() );
        ggPathFields[ j ].setPath( new File( dirName, fileName ));
        affp.automaticFileSuffix( ggPathFields[ j ] );
        if( (protoType[ j ].file == null) || asCopySettings || selectionSettings ) {  // create non-existent file name
          ggPathFields[ j ].setPath( IOUtil.nonExistentFileVariant( ggPathFields[ j ].getPath(), -1,
            selectionSettings ? null : " ", null ));
        }
        ggPathFields[ j ].selectFileName( false );
        msgPane.gridAdd( ggPathFields[ j ], 1, y );
        if( !setFocus ) {
          GUIUtil.setInitialDialogFocus( ggPathFields[ j ]);
          setFocus = true;
        }
      }
      lb = new JLabel( getResourceString( "labelFormat" ), RIGHT );
      msgPane.gridAdd( lb, 0, y );
      msgPane.gridAdd( affp, 1, y, -1, 1 );
      lb.setLabelFor( affp );
      y++;
     
      if( asCopySettings ) {
        ggOpenAfterSave = new JCheckBox( getResourceString( "labelOpenAfterSave" ));
        ggOpenAfterSave.setSelected( openAfterSaveSettings.isSet() );
        msgPane.gridAdd( ggOpenAfterSave, 1, y );
      } else {
        ggOpenAfterSave  = null;
//        msgPane.gridAdd( new JLabel( " " ), 1, y );
      }

      AbstractWindowHandler.setDeepFont( msgPane );
      msgPane.makeCompactGrid();
      p      = new JPanel( new BorderLayout() );
      p.add( msgPane, BorderLayout.NORTH );

      final JOptionPane op = new JOptionPane( p, JOptionPane.QUESTION_MESSAGE, JOptionPane.YES_NO_OPTION, null, queryOptions, queryOptions[ 0 ]);
//      result    = JOptionPane.showOptionDialog( getWindow(), p, getValue( NAME ).toString(),
//                            JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE,
//                            null, queryOptions, queryOptions[ 0 ]);
      result = BasicWindowHandler.showDialog( op, getWindow(), getValue( NAME ).toString() );

      if( ggOpenAfterSave != null ) {
        openAfterSaveSettings.set( ggOpenAfterSave.isSelected() );
      }

      if( result == 0 ) {
        // save dir prefs
        if( ggPathFields.length > 0 ) {
          app.getUserPrefs().put( prefsDirKey, ggPathFields[ 0 ].getPath().getParent() );
        }
       
        afds = new AudioFileDescr[ filesUsed ];
        for( int j = 0, k = 0; j < ggPathFields.length; j++ ) {
          if( channelsUsed[ j ] == 0 ) continue;
          f = ggPathFields[ j ].getPath();
          if( f.exists() ) {
            queryOptions = new String[] { getResourceString( "buttonOverwrite" ),
                            getResourceString( "buttonCancel" )};
            final JOptionPane op2 = new JOptionPane( getResourceString( "warnFileExists" ) +
                                      ":\n" + f.getAbsolutePath() + "\n" + getResourceString( "warnOverwriteFile" ),
                                      JOptionPane.WARNING_MESSAGE, JOptionPane.YES_NO_OPTION,
                                      null, queryOptions, queryOptions[1] );
//            result = JOptionPane.showOptionDialog( getWindow(), getResourceString( "warnFileExists" ) +
//              ":\n" + f.getAbsolutePath() + "\n" + getResourceString( "warnOverwriteFile" ),
//              getValue( NAME ).toString(), JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE,
//              null, queryOptions, queryOptions[1] );
            result = BasicWindowHandler.showDialog( op2, getWindow(), getValue( NAME ).toString() );
            if( result != 0 ) return null;
          }
          afds[ k ]    = new AudioFileDescr( protoType[ j ]);
          affp.toDescr( afds[ k ]);
          afds[ k ].file  = f;
          afds[ k ].channels = channelsUsed[ j ];
          k++;
        }
        return afds;
      } else {
        return null;
      }
    }
  }

//  private class actionImportMarkersClass
//  extends MenuAction
//  {
//    public void actionPerformed( ActionEvent e )
//    {
//      new ImportMarkersDialog( doc );
//    }
//  }

  private class ActionSelectAll
  extends MenuAction
  {
    protected ActionSelectAll() { /* empty */ }

    public void actionPerformed( ActionEvent e )
    {
      doc.timeline.editSelect( this, new Span( 0, timelineLen ));
    }
  }

  private class ActionInsertRec
  extends MenuAction
  {
    protected ActionInsertRec() { /* empty */ }

    public void actionPerformed( ActionEvent e )
    {
      final RecorderDialog    recDlg;
      final File          recFile;
      final Session        tmpDoc;
      final ClipboardTrackList  tl;
      final ProcessingThread    pt;
      final Session        targetDoc;
      final AudioFileDescr    afd, afd2;
   
      try {
        recDlg  = new RecorderDialog( doc );
      }
      catch( IOException e1 ) {
        BasicWindowHandler.showErrorDialog( getWindow(), e1, getValue( NAME ).toString() );
        return;
      }
      recFile  = recDlg.getResult();
     
      if( recFile != null ) {
        try {
//          if( true ) throw new IOException( "test" );
          tmpDoc  = Session.newFrom( recFile, false, false );
        }
        catch( IOException e1 ) {
          GUIUtil.displayError( getWindow(), e1, getValue( NAME ).toString() );
          confirmDelete( recFile );
          return;
        }
        tl    = new ClipboardTrackList( tmpDoc, new Span( 0, tmpDoc.timeline.getLength() ), tmpDoc.tracks.getAll() );
        if( doc.checkProcess()) {
          targetDoc      = doc;
        } else // doc busy, save the recording by creating a separate document
          afd          = doc.getDisplayDescr();
          afd2        = new AudioFileDescr();
          afd2.bitsPerSample  = afd.bitsPerSample;
          afd2.channels    = afd.channels;
          afd2.rate      = afd.rate;
          afd2.sampleFormat  = afd.sampleFormat;
          targetDoc      = ((MenuFactory) app.getMenuFactory()).newDocument( afd2 );
        }
//pt = null;
        pt = targetDoc == null ? null : targetDoc.pasteTrackList( tl, targetDoc.timeline.getPosition(), getValue( NAME ).toString(), targetDoc.getEditMode() );

        if( pt != null ) {
          pt.addListener( new ProcessingThread.Listener() {
            public void processStarted( ProcessingThread.Event e1 ) { /* ignored */ }
            public void processStopped( ProcessingThread.Event e2 )
            {
              tl.dispose();
              tmpDoc.dispose();
              if( e2.isDone() ) {
                deleteFile( recFile );
              } else {
                confirmDelete( recFile );
              }
            }
          });
          targetDoc.start( pt );
        } else {
          tl.dispose();
          tmpDoc.dispose();
          confirmDelete( recFile );
        }
      }
    }
   
    protected void confirmDelete( File path )
    {
      final int    choice;
      final Object[]  options  = new String[] { getResourceString( "buttonKeepFile" ), getResourceString( "buttonDeleteFile" )};
      final JOptionPane op = new JOptionPane( getResourceString( "optionDlgKeepRec1" ) + path.getAbsolutePath() + getResourceString( "optionDlgKeepRec2" ),
                                              JOptionPane.ERROR_MESSAGE, JOptionPane.YES_NO_OPTION, null, options, options[ 0 ]);
//      choice = JOptionPane.showOptionDialog( getWindow(), getResourceString( "optionDlgKeepRec1" ) + path.getAbsolutePath() + getResourceString( "optionDlgKeepRec2" ),
//             getValue( NAME ).toString(), JOptionPane.YES_NO_OPTION, JOptionPane.ERROR_MESSAGE, null,
//             options, options[ 0 ]);
      choice = BasicWindowHandler.showDialog( op, getWindow(), getValue( NAME ).toString() );
      if( choice == 1 ) {
        deleteFile( path );
      }
    }
   
    protected void deleteFile( File path )
    {
      if( !path.delete() ) {
        final JOptionPane op = new JOptionPane( path.getAbsolutePath() + ":\n" + getResourceString( "errDeleteFile" ), JOptionPane.WARNING_MESSAGE );
//        JOptionPane.showMessageDialog( getWindow(), path.getAbsolutePath() + ":\n" + getResourceString( "errDeleteFile" ), getValue( NAME ).toString(),
//          JOptionPane.WARNING_MESSAGE );
        BasicWindowHandler.showDialog( op, getWindow(), getValue( NAME ).toString() );
      }
    }
  } // class actionInsertRecClass
 
  private class ActionProcess
  extends MenuAction
  {
    protected ActionProcess() { /* empty */ }

    public void actionPerformed( ActionEvent e ) { /* empty */ }
  }

  private class ActionPlugIn
  extends MenuAction
  {
    private final String plugInClassName;
   
    protected ActionPlugIn( String plugInClassName )
    {
      this.plugInClassName  = plugInClassName;
    }

    public void actionPerformed( ActionEvent e )
    {
      FilterDialog filterDlg = (FilterDialog) app.getComponent( Main.COMP_FILTER );
   
      if( filterDlg == null ) {
        filterDlg = new FilterDialog();
      }
      filterDlg.process( plugInClassName, doc, (e.getModifiers() & ActionEvent.SHIFT_MASK) != 0, false );
      actionProcessAgain.setPlugIn( filterDlg.getPlugIn() );
    }
  }

  private class ActionProcessAgain
  extends MenuAction
  {
    private String plugInClassName = null;
 
    protected ActionProcessAgain()
    {
      super();
      setEnabled( false );
    }

    public void actionPerformed( ActionEvent e )
    {
      if( plugInClassName == null ) return;
     
      FilterDialog filterDlg = (FilterDialog) app.getComponent( Main.COMP_FILTER );
     
      if( filterDlg == null ) {
        filterDlg = new FilterDialog();
      }
      filterDlg.process( plugInClassName, doc, false, true );
    }
   
    protected void setPlugIn( RenderPlugIn plugIn )
    {
      if( plugIn == null ) {
        if( isEnabled() ) {
          setEnabled( false );
          putValue( NAME, getResourceString( "menuProcessAgain" ));
        }
        plugInClassName = null;
      } else {
        if( !isEnabled() ) {
          setEnabled( true );
        }
        putValue( NAME, getResourceString( "menuProcessAgain" ) + " : " + plugIn.getName() );
        plugInClassName = plugIn.getClass().getName();
      }
    }
  }

  private class ActionAudioInfo
  extends MenuAction
  {
    protected ActionAudioInfo() { /* empty */ }
   
    /**
     *  Brings up the Audio-Info-Box
     */
    public void actionPerformed( ActionEvent e )
    {
      AudioFileInfoPalette infoBox = (AudioFileInfoPalette) app.getComponent( Main.COMP_AUDIOINFO );
   
      if( infoBox == null ) {
        infoBox = new AudioFileInfoPalette();
      }
      infoBox.setVisible( true );
      infoBox.toFront();
    }
  }

  private class ActionRevealFile
  extends MenuAction
  {
    private File f;
 
    protected ActionRevealFile()
    {
      super( "Reveal File in Finder" );
      setFile( null );
    }
 
    // osascript -e 'tell application "Finder"' -e 'activate' -e 'open location "file:///Volumes/Claude/audio"'
    //   -e 'select file "Sine441HzGain.aif" of folder of the front window'
    //   -e 'end tell'

    /**
     *  Shows the file in the finder (Mac OS only)
     */
    public void actionPerformed( ActionEvent e )
    {
      if( f == null ) return;
     
      try {
        // File.toURL() ends up with sth. like "file:/Volumes/Claude/..." omitting the two initial slashes
//        final String[] cmdArray = { "osascript", "-e", "tell application \"Finder\"", "-e", "activate",
//                      "-e", "open location \"" + f.getAbsoluteFile().toURL().toString() + "\"",
//                      "-e", "end tell" };
        // make sure space characters are escaped as %20 in URL stylee
        final Normalizer n    = new Normalizer( Normalizer.C, false );
//        final String parentDir  = n.normalize( f.getParentFile().getAbsolutePath() ).replaceAll( " ", "%20" );
        String path        = n.normalize( f.getParentFile().getAbsoluteFile().toURI().toURL().toExternalForm() ); // getAbsolutePath() ).replaceAll( " ", "%20" );
//        String path        = n.normalize( f.getAbsoluteFile().toURL().toExternalForm() ); // getAbsolutePath() ).replaceAll( " ", "%20" );
        path          = path.substring( 5 );
StringBuffer sb = new StringBuffer();
//char ch;
int  chI;
byte[] hex = "0123456789abcdef".getBytes();
byte[] enc = path.getBytes( "UTF-8" );
for( int i = 0; i < enc.length; i++ ) {
  chI = enc[ i ]; // parentDir.charAt( i );
//  chI = (int) ch;
  if( (chI < 33) || (chI > 127) ) {
    sb.append( "%" + (char) hex[ (chI >> 4) & 0x0F ] + (char) hex[ chI & 0x0F ]);
  } else {
    sb.append( (char) chI );
  }
}
path = sb.toString();
//int i = path.lastIndexOf( '/' ) + 1;
final String parentDir = path;
final String fileName = n.normalize( f.getName() ); // .getBytes( "ISO-8859-1" ));
//final String parentDir = path.substring( 0, i );
//final String fileName = path.substring( i );
//System.err.println( "'" + parentDir + "'" );       
//System.err.println( "'" + fileName + "'" );       
        final String[] cmdArray = { "osascript", "-e", "tell application \"Finder\"", "-e", "activate",
                      "-e", "open location \"file://" + parentDir + "\"",
//                      "-e", "select location \"file://" + parentDir + "\"",
//                      "-e", "select file \"" + f.getName() + "\" of folder of the front window",
//                      "-e", "select location \"" + fileName + "\" of folder of the front window",
                      "-e", "select file \"" + fileName + "\" of folder of the front window",
                      "-e", "end tell" };
//for( int i = 0; i < cmdArray.length; i++ ) {
//  System.err.println( "#" + cmdArray[i] + "#" );
//}
     
        Runtime.getRuntime().exec( cmdArray, null, null );
      }
      catch( IOException e1 ) {
        displayError( e1, getValue( NAME ).toString() );
      }
    }
   
    protected void setFile( File f )
    {
      this.f  = f;
      setEnabled( f != null );
    }
  }

//  private abstract class ActionVerticalZoom
//  extends AbstractAction
//  {
//    protected ActionVerticalZoom() { /* empty */ }
//  }
 
  /**
   *  Increase or decrease the vertical
   *  range of the waveform display
   */
  private class ActionVerticalMax
  extends AbstractAction
  {
    private final float linFactor;
    private final float logOffset;
   
    /**
     *  @param  linFactor  factors > 1 increase the row height,
     *          factors < 1 decrease.
     */
    protected ActionVerticalMax( float linFactor, float logOffset )
    {
      super();
      this.linFactor = linFactor;
      this.logOffset = logOffset;
    }
   
    public void actionPerformed( ActionEvent e )
    {
      if( waveView.getVerticalScale() == PrefsUtil.VSCALE_AMP_LIN ) zoomLin(); else zoomLog();
    }
   
    private void zoomLin()
    {
      float min, max;

      min = waveView.getAmpLinMin();
      max = waveView.getAmpLinMax();

      if( ((linFactor >= 1.0f) && (min > -1.0e6f) && (max < 1.0e6f)) || (linFactor < 1.0f && (min < -1.0e-4f) && (max > 1.0e-4f)) ) {
        min     *= linFactor;
        max     *= linFactor;
        waveView.setAmpLinMinMax( min, max );
        updateVerticalRuler();
      }
    }

    private void zoomLog()
    {
      float min, max;

      min = waveView.getAmpLogMin();
      max = waveView.getAmpLogMax();

      if( (max + logOffset - min >= 6f) &&
        (((logOffset >= 0f) && (max < 60)) || (logOffset < 0f && (max > -160))) ) {
//        min     += logOffset;
        max     += logOffset;
        waveView.setAmpLogMinMax( min, max );
        updateVerticalRuler();
      }
    }
  } // class actionVerticalMax

  /**
   *  Increase or decrease the vertical
   *  noisefloor of the waveform display (in log mode)
   */
  private class ActionVerticalMin
  extends AbstractAction
  {
    private final float logOffset;
   
    /**
     */
    protected ActionVerticalMin( float logOffset )
    {
      super();
      this.logOffset = logOffset;
    }
   
    public void actionPerformed( ActionEvent e )
    {
      if( waveView.getVerticalScale() != PrefsUtil.VSCALE_AMP_LIN ) zoomLog();
    }
   
    private void zoomLog()
    {
      float min, max;

      min = waveView.getAmpLogMin();
      max = waveView.getAmpLogMax();

      if( (max - (min + logOffset) >= 6f) &&
        (((logOffset >= 0f) && (min < 60)) || (logOffset < 0f && (max > -160))) ) {
        min     += logOffset;
//        max     += logOffset;
        waveView.setAmpLogMinMax( min, max );
        updateVerticalRuler();
      }
    }
  } // class actionVerticalMin

  /**
   *  Increase or decrease the width
   *  of the visible time span
   */
  private class ActionSpanWidth
  extends AbstractAction
  {
    private final float factor;
   
    /**
     *  @param  factor  factors > 1 increase the span width (zoom out)
     *          factors < 1 decrease (zoom in).
     *          special value 0.0 means zoom to sample level
     */
    protected ActionSpanWidth( float factor )
    {
      super();
      this.factor = factor;
    }
   
    public void actionPerformed( ActionEvent e )
    {
      perform();
    }
   
    public void perform()
    {
      long  pos, visiLen, start, stop;
      Span  visiSpan;
     
      visiSpan  = timelineVis;
      visiLen    = visiSpan.getLength();
      pos      = timelinePos; // doc.timeline.getPosition();
      if( factor == 0.0f ) {        // to sample level
        start  = Math.max( 0, pos - (wavePanel.getWidth() >> 1) );
        stop  = Math.min( timelineLen, start + wavePanel.getWidth() );
      } else if( factor < 1.0f ) {    // zoom in
        if( visiLen < 4 ) return;
        // if timeline pos visible -> try to keep it's relative position constant
        if( visiSpan.contains( pos )) {
          start  = pos - (long) ((pos - visiSpan.getStart()) * factor + 0.5f);
          stop    = start + (long) (visiLen * factor + 0.5f);
        // if timeline pos before visible span, zoom left hand
        } else if( visiSpan.getStart() > pos ) {
          start  = visiSpan.getStart();
          stop    = start + (long) (visiLen * factor + 0.5f);
        // if timeline pos after visible span, zoom right hand
        } else {
          stop  = visiSpan.getStop();
          start   = stop - (long) (visiLen * factor + 0.5f);
        }
      } else {      // zoom out
        start   = Math.max( 0, visiSpan.getStart() - (long) (visiLen * factor/4 + 0.5f) );
        stop    = Math.min( timelineLen, start + (long) (visiLen * factor + 0.5f) );
      }
      visiSpan  = new Span( start, stop );
      if( !visiSpan.isEmpty() ) {
        doc.timeline.editScroll( this, visiSpan );
      }
    }
  } // class actionSpanWidthClass

  private static final int SCROLL_SESSION_START  = 0;
  private static final int SCROLL_SELECTION_START  = 1;
  private static final int SCROLL_SELECTION_STOP  = 2;
  private static final int SCROLL_FIT_TO_SELECTION= 3;
  private static final int SCROLL_ENTIRE_SESSION  = 4;

  private class ActionScroll
  extends AbstractAction
  {
    private final int mode;
 
    protected ActionScroll( int mode )
    {
      super();
     
      this.mode = mode;
    }
 
    public void actionPerformed( ActionEvent e )
    {
      perform();
    }
   
    public void perform()
    {
      UndoableEdit  edit  = null;
      Span      selSpan, newSpan;
      long      start, stop;
   
      if( mode == SCROLL_SESSION_START && transport.isRunning() ) {
        transport.stop();
      }
      selSpan    = timelineSel; // doc.timeline.getSelectionSpan();
     
      switch( mode ) {
      case SCROLL_SESSION_START:
        if( timelinePos != 0 ) {
          edit  = TimelineVisualEdit.position( this, doc, 0 ).perform();
          if( !timelineVis.contains( 0 )) {
            final CompoundEdit ce  = new BasicCompoundEdit();
            ce.addEdit( edit );
            newSpan  = new Span( 0, timelineVis.getLength() );
            ce.addEdit( TimelineVisualEdit.scroll( this, doc, newSpan ).perform() );
            ce.end();
            edit  = ce;
          }
        }
        break;
       
      case SCROLL_SELECTION_START:
        if( selSpan.isEmpty() ) selSpan = new Span( timelinePos, timelinePos );
        if( timelineVis.contains( selSpan.getStart() )) {
          start = Math.max( 0, selSpan.getStart() - (timelineVis.getLength() >> 1) );
        } else {
          start = Math.max( 0, selSpan.getStart() - (timelineVis.getLength() >> 3) );
        }
        stop  = Math.min( timelineLen, start + timelineVis.getLength() );
        newSpan  = new Span( start, stop );
        if( !timelineVis.equals( newSpan ) && !newSpan.isEmpty() ) {
          edit  = TimelineVisualEdit.scroll( this, doc, newSpan ).perform();
        }
        break;

      case SCROLL_SELECTION_STOP:
        if( selSpan.isEmpty() ) selSpan = new Span( timelinePos, timelinePos );
        if( timelineVis.contains( selSpan.getStop() )) {
          stop = Math.min( timelineLen, selSpan.getStop() + (timelineVis.getLength() >> 1) );
        } else {
          stop = Math.min( timelineLen, selSpan.getStop() + (timelineVis.getLength() >> 3) );
        }
        start  = Math.max( 0, stop - timelineVis.getLength() );
        newSpan  = new Span( start, stop );
        if( !timelineVis.equals( newSpan ) && !newSpan.isEmpty() ) {
          edit  = TimelineVisualEdit.scroll( this, doc, newSpan ).perform();
        }
        break;

      case SCROLL_FIT_TO_SELECTION:
        newSpan    = selSpan;
        if( !timelineVis.equals( newSpan ) && !newSpan.isEmpty() ) {
          edit  = TimelineVisualEdit.scroll( this, doc, newSpan ).perform();
        }
        break;

      case SCROLL_ENTIRE_SESSION:
        newSpan    = new Span( 0, timelineLen );
        if( !timelineVis.equals( newSpan ) && !newSpan.isEmpty() ) {
          edit  = TimelineVisualEdit.scroll( this, doc, newSpan ).perform();
        }
        break;

      default:
        assert false : mode;
        break;
      }
      if( edit != null ) doc.getUndoManager().addEdit( edit );
    }
  } // class actionScrollClass
 
  private static final int SELECT_TO_SESSION_START  = 0;
  private static final int SELECT_TO_SESSION_END    = 1;

  private class ActionSelect
  extends AbstractAction
  {
    private final int mode;
 
    protected ActionSelect( int mode )
    {
      super();
     
      this.mode = mode;
    }
 
    public void actionPerformed( ActionEvent e )
    {
      Span      selSpan, newSpan = null;
   
      selSpan    = timelineSel; // doc.timeline.getSelectionSpan();
      if( selSpan.isEmpty() ) {
        selSpan  = new Span( timelinePos, timelinePos );
      }
     
      switch( mode ) {
      case SELECT_TO_SESSION_START:
        if( selSpan.getStop() > 0 ){
          newSpan = new Span( 0, selSpan.getStop() );
        }
        break;

      case SELECT_TO_SESSION_END:
        if( selSpan.getStart() < timelineLen ){
          newSpan = new Span( selSpan.getStart(), timelineLen );
        }
        break;

      default:
        assert false : mode;
        break;
      }
      if( newSpan != null && !newSpan.equals( selSpan )) {
        doc.timeline.editSelect( this, newSpan );
//          doc.getUndoManager().addEdit( TimelineVisualEdit.select( this, doc, newSpan ));
      }
    }
  } // class actionSelectClass
     
  private static final int SELECT_NEXT_REGION  = 0;
  private static final int SELECT_PREV_REGION  = 1;
  private static final int EXTEND_NEXT_REGION  = 2;
  private static final int EXTEND_PREV_REGION  = 3;

  private class ActionSelectRegion
  extends AbstractAction
  {
    private final int mode;
 
    protected ActionSelectRegion( int mode )
    {
      super();
     
      this.mode = mode;
    }
 
    public void actionPerformed( ActionEvent e )
    {
      Span      selSpan;
      UndoableEdit  edit;
      long      start, stop;
      Marker      mark;
      int        idx;

      if( !markVisible ) return;
   
      selSpan    = timelineSel; // doc.timeline.getSelectionSpan();
      if( selSpan.isEmpty() ) selSpan = new Span( timelinePos, timelinePos );
     
      start    = selSpan.getStart();
      stop    = selSpan.getStop();
     
      switch( mode ) {
      case SELECT_NEXT_REGION:
      case EXTEND_NEXT_REGION:
        idx    = doc.markers.indexOf( stop + 1 )// XXX check
        if( idx < 0 ) idx = -(idx + 1);

        ifidx == doc.markers.getNumStakes() ) {
          stop  = timelineLen;
        } else {
          mark  = doc.markers.get( idx );
          stop  = mark.pos;
        }
        // (-(insertion point) - 1)

        if( mode == SELECT_NEXT_REGION ) {
          idx    = doc.markers.indexOf( stop - 1 )// XXX check
          if( idx < 0 ) idx = -(idx + 2);
         
          if( idx == -1 ) {
            start  = 0;
          } else {
            mark  = doc.markers.get( idx );
            start  = mark.pos;
          }
        }
        break;

      case SELECT_PREV_REGION:
      case EXTEND_PREV_REGION:
        idx    = doc.markers.indexOf( start - 1 )// XXX check
        if( idx < 0 ) idx = -(idx + 2);

        ifidx == -1 ) {
          start  = 0;
        } else {
          mark  = doc.markers.get( idx );
          start  = mark.pos;
        }
       
        if( mode == SELECT_PREV_REGION ) {
          idx    = doc.markers.indexOf( start + 1 )// XXX check
          if( idx < 0 ) idx = -(idx + 1);
         
          if( idx == doc.markers.getNumStakes() ) {
            stop  = timelineLen;
          } else {
            mark  = doc.markers.get( idx );
            stop  = mark.pos;
          }
        }
        break;

      default:
        assert false : mode;
        break;
      }
     
      if( (start == selSpan.getStart()) && (stop == selSpan.getStop()) ) return;
     
      edit  = TimelineVisualEdit.select( this, doc, new Span( start, stop )).perform();
      doc.getUndoManager().addEdit( edit );
    }
  } // class actionSelectRegionClass
   
  private class ActionDropMarker
  extends AbstractAction
  {
    protected ActionDropMarker() { /* empty */ }

    public void actionPerformed( ActionEvent e )
    {
      if( markVisible ) {
        markAxis.addMarker( timelinePos );
      }
    }
  } // class actionDropMarkerClass

// -------------- AFR Transfer Handler --------------

  private class AFRTransferHandler
  extends TransferHandler
  {
    protected AFRTransferHandler() { /* empty */ }

    public int getSourceActions( JComponent c )
    {
      return COPY;
    }
   
    protected Transferable createTransferable( JComponent c )
    {
//      return new StringSelection( doc.getAudioFileDescr().file.getAbsolutePath() + File.pathSeparator +
//        doc.timeline.getSelectionSpan().getStart() + File.pathSeparator +
//        doc.timeline.getSelectionSpan().getStop() );
      return new StringSelection( doc.getDisplayDescr().file.getAbsolutePath() + File.pathSeparator +
        doc.timeline.getSelectionSpan().getStart() + File.pathSeparator +
        doc.timeline.getSelectionSpan().getStop() );
   
//      System.err.println( "createTransferable" );
//      return new AudioFileRegion( new File( "haschimoto" ), new Span( 42, 43 ));
//   
//      final Span          span;
//      final AudioTrail  mte;
//
//      if( !doc.bird.attemptShared( Session.DOOR_TIME | Session.DOOR_MTE, 250 )) return null;
//      try {
//        span = timelineSel; // doc.timeline.getSelectionSpan();
//        if( span.isEmpty() ) return null;
//
//        mte = doc.getAudioTrail();
//        return mte.getSampledChunkList( span );
//      }
//      finally {
//        doc.bird.releaseShared( Session.DOOR_TIME | Session.DOOR_MTE );
//      }
    }
   
    protected void exportDone( JComponent source, Transferable data, int action )
    {
       /* ignored */
    }

//    public boolean canImport( JComponent c, DataFlavor[] flavors )
//    {
//      return false;
//    }
  }

  private abstract class TimelineTool
  extends AbstractTool
  {
    private final List  collObservedComponents  = new ArrayList();
 
    private boolean adjustCatchBypass  = false;
   
    protected TimelineTool() { /* empty */ }

    public void toolAcquired( Component c )
    {
      super.toolAcquired( c );
     
      if( c instanceof Container ) addMouseListeners( (Container) c );
    }
   
    // additionally installs mouse input listeners on child components
    private void addMouseListeners( Container c )
    {
      Component  c2;
     
      for( int i = 0; i < c.getComponentCount(); i++ ) {
        c2 = c.getComponent( i );
        collObservedComponents.add( c2 );
        c2.addMouseListener( this );
        c2.addMouseMotionListener( this );
        if( c2 instanceof Container ) addMouseListeners( (Container) c2 )// recurse
      }
    }
   
    // additionally removes mouse input listeners from child components
    private void removeMouseListeners()
    {
      Component  c;
   
      while( !collObservedComponents.isEmpty() ) {
        c  = (Component) collObservedComponents.remove( 0 );
        c.removeMouseListener( this );
        c.removeMouseMotionListener( this );
      }
    }

    public void toolDismissed( Component c )
    {
      super.toolDismissed( c );

      removeMouseListeners();
     
      if( adjustCatchBypass ) {
        adjustCatchBypass = false;
        removeCatchBypass();
      }
    }
   
    public void mousePressed( MouseEvent e )
    {
      adjustCatchBypass = true;
      addCatchBypass();
     
      super.mousePressed( e );
    }
   
    public void mouseReleased( MouseEvent e )
    {
      adjustCatchBypass = false;
      removeCatchBypass();
     
      super.mouseReleased( e );
    }
  }
 
  /*
   *  Keyboard modifiers are consistent with Bias Peak:
   *  Shift+Click = extend selection, Meta+Click = select all,
   *  Alt+Drag = drag timeline position; double-click = Play
   */
  private class TimelinePointerTool
  extends TimelineTool
  {
    private boolean shiftDrag, ctrlDrag, dragStarted = false;
    protected boolean validDrag = false;
    private long startPos;
    private int startX;

    private final Object[] argsCsr  = new Object[8];
    private final String[] csrInfo  = new String[3];
 
    protected TimelinePointerTool() { /* empty */ }

    public void paintOnTop( Graphics2D g )
    {
      // not necessary
    }
   
    protected void cancelGesture()
    {
      dragStarted = false;
      validDrag  = false;
    }
   
    public void mousePressed( MouseEvent e )
    {
      super.mousePressed( e );
   
      if( e.isMetaDown() ) {
        selectRegion( e );
        dragStarted = false;
        validDrag  = false;
      } else {
        shiftDrag  = e.isShiftDown();
        ctrlDrag  = e.isControlDown();
        dragStarted = false;
        validDrag  = true;
        startX    = e.getX();
        processDrag( e, false );
      }
    }

    public void mouseDragged( MouseEvent e )
    {
      final ObserverPalette observer;
     
      super.mouseDragged( e );

      if( validDrag ) {
        if( !dragStarted ) {
          if( shiftDrag || ctrlDrag || Math.abs( e.getX() - startX ) > 2 ) {
            dragStarted = true;
          } else return;
        }
        processDrag( e, true );
      }
     
      // cursor information
      observer = (ObserverPalette) app.getComponent( Main.COMP_OBSERVER );
      if( (observer != null) && observer.isVisible() && (observer.getShownTab() == ObserverPalette.CURSOR_TAB) ) {       
        showCursorInfo( SwingUtilities.convertPoint( e.getComponent(), e.getPoint(), waveView ));
      }
    }
   
    private void showCursorInfo( Point screenPt )
    {
      final ObserverPalette  observer;
     
      final int        ch    = waveView.channelForPoint( screenPt );
      if( ch == -1 ) return;

      final DecimationInfo  info  = waveView.getDecimationInfo();
      if( info == null ) return;

      final long        pos    = timelineVis.getStart() + (long)
                    ((double) screenPt.x / (double) waveView.getWidth() *
                     timelineVis.getLength());
      if( (pos < 0) || (pos >= timelineLen) ) return;
   
      final String      chName  = doc.audioTracks.get( ch ).getName();
      final double      seconds  = pos / timelineRate;
      final AudioTrail     at;
      final DecimatedWaveTrail  dt;
      final float[][]      data;
      final float[]      frame;
      float          f1;
     
      argsCsr[3]    = chName;
      argsCsr[0]    = new Long( pos );
      argsCsr[1]    = new Integer( (int) (seconds / 60) );
      argsCsr[2]    = new Float( seconds % 60 );
     
      csrInfo[0]    = msgCsr1.format( argsCsr );
     
      switch( info.model ) {
      case DecimatedTrail.MODEL_PCM:
        at      = doc.getAudioTrail();
        data    = new float[ at.getChannelNum() ][];
        data[ ch = new float[ 1 ];
        try {
          at.readFrames( data, 0, new Span( pos, pos + 1 ));
        }
        catch( IOException e1 ) { return; }
        f1      = data[ ch ][ 0 ];
        argsCsr[4= new Float( f1 );
        argsCsr[5= new Float( Math.log( Math.abs( f1 )) * TWENTYDIVLOG10 );
        csrInfo[1= msgCsr2PCMFloat.format( argsCsr );
        if( csrInfoIsInt ) {
          argsCsr[6= new Long( (long) (f1 * (1L << (csrInfoBits - 1))) );
          argsCsr[7= new Integer( csrInfoBits );
          csrInfo[2= msgCsr3PCMInt.format( argsCsr );
        } else {
          csrInfo[2= "";
        }
        break;
       
      case DecimatedTrail.MODEL_FULLWAVE_PEAKRMS:
        dt      = doc.getDecimatedWaveTrail();
        if( dt == null ) return;
        frame    = new float[ dt.getNumModelChannels() ];
        try {
          dt.readFrame( Math.min( dt.getNumDecimations() - 1, info.idx + 1 ), pos, ch, frame );
        }
        catch( IOException e1 ) { return; }
        f1      = Math.max( frame[ 0 ], -frame[ 1 ] )// peak pos/neg
        argsCsr[4= new Float( f1 );
        argsCsr[5= new Float( Math.log( f1 ) * TWENTYDIVLOG10 );
        f1      = (float) Math.sqrt( frame[ 2 ])// mean sqr pos/neg
        argsCsr[6= new Float( f1 );
        argsCsr[7= new Float( Math.log( f1 ) * TWENTYDIVLOG10 );
        csrInfo[1= msgCsr2Peak.format( argsCsr );
        csrInfo[2= msgCsr3RMS.format( argsCsr );
        break;
       
      default:
        return;
      }

      observer = (ObserverPalette) app.getComponent( Main.COMP_OBSERVER );
      if( observer != null ) observer.showCursorInfo( csrInfo );
    }
   
    private void selectRegion( MouseEvent e )
    {
      final Point pt  = SwingUtilities.convertPoint( e.getComponent(), e.getPoint(), wavePanel );

      Span      span, span2;
      long      pos, start, stop;
      UndoableEdit  edit;
      int        idx;
      Marker      mark;

      span        = timelineVis; // doc.timeline.getVisibleSpan();
      span2    = timelineSel; // doc.timeline.getSelectionSpan();
      pos      = span.getStart() + (long) (pt.getX() / getComponent().getWidth() *
                          span.getLength());
      pos      = Math.max( 0, Math.min( timelineLen, pos ));

      stop    = timelineLen;
      start    = 0;

      if( markVisible ) {
        idx    = doc.markers.indexOf( pos + 1 )// XXX check
        if( idx < 0 ) idx = -(idx + 1);
        ifidx < doc.markers.getNumStakes() ) {
          mark  = doc.markers.get( idx );
          stop  = mark.pos;
        }
        idx    = doc.markers.indexOf( stop - 1 )// XXX check
        if( idx < 0 ) idx = -(idx + 2);
        if( idx >= 0 ) {
          mark  = doc.markers.get( idx );
          start  = mark.pos;
        }
      }
     
      // union with current selection
      if( e.isShiftDown() && !span2.isEmpty() ) {
        start  = Math.min( start, span2.start );
        stop  = Math.max( stop, span2.stop );
      }
     
      span  = new Span( start, stop );
      if( span.equals( span2 )) {
        span  = new Span( 0, timelineLen );
      }
      if( !span.equals( span2 )) {
        edit = TimelineVisualEdit.select( this, doc, span ).perform();
        doc.getUndoManager().addEdit( edit );
      }
    }

    private void processDrag( MouseEvent e, boolean hasStarted )
    {
      final Point pt  = SwingUtilities.convertPoint( e.getComponent(), e.getPoint(), wavePanel );
     
      Span      span, span2;
      long      position;
      UndoableEdit  edit;
      
      span        = timelineVis; // doc.timeline.getVisibleSpan();
      span2    = timelineSel; // doc.timeline.getSelectionSpan();
      position    = span.getStart() + (long) (pt.getX() / getComponent().getWidth() *
                          span.getLength());
      position    = Math.max( 0, Math.min( timelineLen, position ));
      if( !hasStarted && !ctrlDrag ) {
        if( shiftDrag ) {
          if( span2.isEmpty() ) {
            span2 = new Span( timelinePos, timelinePos );
          }
          startPos = Math.abs( span2.getStart() - position ) >
                 Math.abs( span2.getStop() - position ) ?
                  span2.getStart() : span2.getStop();
          span2  = new Span( Math.min( startPos, position ),
                    Math.max( startPos, position ));
          edit  = TimelineVisualEdit.select( this, doc, span2 ).perform();
        } else {
          startPos = position;
          if( span2.isEmpty() ) {
            edit = TimelineVisualEdit.position( this, doc, position ).perform();
          } else {
            edit = new CompoundEdit();
            edit.addEdit( TimelineVisualEdit.select( this, doc, new Span() ).perform() );
            edit.addEdit( TimelineVisualEdit.position( this, doc, position ).perform() );
            ((CompoundEdit) edit).end();
          }
        }
      } else {
        if( ctrlDrag ) {
          edit  = TimelineVisualEdit.position( this, doc, position ).perform();
        } else {
          span2  = new Span( Math.min( startPos, position ),
                    Math.max( startPos, position ));
          edit  = TimelineVisualEdit.select( this, doc, span2 ).perform();
        }
      }
      doc.getUndoManager().addEdit( edit );
    }

    public void mouseReleased( MouseEvent e )
    {
      super.mouseReleased( e );

      Span span2;

      // resets the position to selection start if (and only if) the selection was
      // made anew, ctrl key is not pressed and transport is not running
      if( dragStarted && !shiftDrag && !ctrlDrag && !transport.isRunning() ) {
        span2 = timelineSel; // doc.timeline.getSelectionSpan();
        if( !span2.isEmpty() && timelinePos != span2.getStart() ) {
          doc.timeline.editPosition( this, span2.getStart() );
        }
      }
     
      dragStarted = false;
      validDrag  = false;
    }

    public void mouseClicked( MouseEvent e )
    {
      super.mouseClicked( e );

      if( (e.getClickCount() == 2) && !e.isMetaDown() && !transport.isRunning() ) {
        transport.play( 1.0f );
      }
    }

    // on Mac, Ctrl+Click is interpreted as
    // popup trigger by the system which means
    // no successive mouseDragged calls are made,
    // instead mouseMoved is called ...
    public void mouseMoved( MouseEvent e )
    {
      super.mouseMoved( e );

      mouseDragged( e );
    }
  }

  private class TimelineZoomTool
  extends TimelineTool
  {
    private boolean          validDrag  = false, dragStarted = false;
    private long          startPos;
    private Point          startPt;
    private long          position;
    private final javax.swing.Timer  zoomTimer;
    protected final Rectangle    zoomRect  = new Rectangle();
    private MenuAction actionZoomIn    = null;
    private MenuAction actionZoomOut  = null;

    protected TimelineZoomTool()
    {
      zoomTimer = new javax.swing.Timer( 250, new ActionListener() {
        public void actionPerformed( ActionEvent e )
        {
          setZoomRect( zoomRect );
        }
      });
    }

    public void toolAcquired( final Component c )
    {
      super.toolAcquired( c );
      c.setCursor( zoomCsr[ 0 ]);
      if( c instanceof JComponent ) {
        final JComponent jc = (JComponent) c;
        if( actionZoomOut == null ) actionZoomOut = new MenuAction( "zoomOut",
          KeyStroke.getKeyStroke( KeyEvent.VK_ALT, InputEvent.ALT_DOWN_MASK, false )) {
          public void actionPerformed( ActionEvent e ) {
            c.setCursor( zoomCsr[ 1 ]);
          }
        };
        if( actionZoomIn == null ) actionZoomIn = new MenuAction( "zoomIn",
           KeyStroke.getKeyStroke( KeyEvent.VK_ALT, 0, true )) {
          public void actionPerformed( ActionEvent e ) {
            c.setCursor( zoomCsr[ 0 ]);
          }
        };
        actionZoomOut.installOn( jc, JComponent.WHEN_IN_FOCUSED_WINDOW );
        actionZoomIn.installOn( jc, JComponent.WHEN_IN_FOCUSED_WINDOW );
      }
    }

    public void toolDismissed( Component c )
    {
      super.toolDismissed( c );
      if( c instanceof JComponent ) {
        final JComponent jc = (JComponent) c;
        if( actionZoomOut != null ) actionZoomOut.deinstallFrom( jc, JComponent.WHEN_IN_FOCUSED_WINDOW );
        if( actionZoomIn != null ) actionZoomIn.deinstallFrom( jc, JComponent.WHEN_IN_FOCUSED_WINDOW );
      }
    }

    public void paintOnTop( Graphics2D g )
    {
      // not necessary
    }
   
    public void mousePressed( MouseEvent e )
    {
      super.mousePressed( e );
   
      if( e.isAltDown() ) {
        dragStarted = false;
        validDrag  = false;
        clickZoom( 2.0f, e );
      } else {
        dragStarted = false;
        validDrag  = true;
        processDrag( e, false );
      }
    }

    public void mouseDragged( MouseEvent e )
    {
      super.mouseDragged( e );

      if( validDrag ) {
        if( !dragStarted ) {
          if( Math.abs( e.getX() - startPt.x ) > 2 ) {
            dragStarted = true;
            zoomTimer.restart();
          } else return;
        }
        processDrag( e, true );
      }
    }

    protected void cancelGesture()
    {
      zoomTimer.stop();
      setZoomRect( null );
      dragStarted = false;
      validDrag  = false;
    }

    public void mouseReleased( MouseEvent e )
    {
      super.mouseReleased( e );

      Span span;

      if( dragStarted ) {
        cancelGesture();
        span = new Span( Math.min( startPos, position ),
                 Math.max( startPos, position ));
        if( !span.isEmpty() ) {
          doc.timeline.editScroll( this, span );
        }
      }
     
      validDrag  = false;
    }

    // zoom to mouse position
    public void mouseClicked( MouseEvent e )
    {
      super.mouseClicked( e );

      if( !e.isAltDown() ) clickZoom( 0.5f, e );
    }
   
    private void clickZoom( float factor, MouseEvent e )
    {
      long  pos, visiLen, start, stop;
      Span  visiSpan;
     
      visiSpan  = timelineVis;
      visiLen    = visiSpan.getLength();
      pos      = visiSpan.getStart() + (long) ((double) e.getX() / (double) getComponent().getWidth() *
                          visiSpan.getLength());
      visiLen    = (long) (visiLen * factor + 0.5f);
      if( visiLen < 2 ) return;
     
      start    = Math.max( 0, Math.min( timelineLen, pos - (long) ((pos - visiSpan.getStart()) * factor + 0.5f) ));
      stop    = start + visiLen;
      if( stop > timelineLen ) {
        stop  = timelineLen;
        start  = Math.max( 0, stop - visiLen );
      }
      visiSpan  = new Span( start, stop );
      if( !visiSpan.isEmpty() ) {
        doc.timeline.editScroll( this, visiSpan );
      }
    }

    private void processDrag( MouseEvent e, boolean hasStarted )
    {
      final Point pt  = SwingUtilities.convertPoint( e.getComponent(), e.getPoint(), wavePanel );
     
      Span  span;
      int    zoomX;
      
      span        = timelineVis;
      position    = span.getStart() + (long) (pt.getX() / getComponent().getWidth() *
                          span.getLength());
      position    = Math.max( 0, Math.min( timelineLen, position ));
      if( !hasStarted ) {
        startPos= position;
        startPt  = pt;
      } else {
        zoomX  = Math.min( startPt.x, pt.x );
        zoomRect.setBounds( zoomX, waveView.getY() + 6, Math.abs( startPt.x - pt.x ), waveView.getHeight() - 12 );
        setZoomRect( zoomRect );
      }
    }
  }
}
TOP

Related Classes of de.sciss.eisenkraut.session.DocumentFrame$ActionDropMarker

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.