10302 lines
318 KiB
C++
10302 lines
318 KiB
C++
/**********************************************************************
|
|
|
|
Audacity: A Digital Audio Editor
|
|
|
|
TrackPanel.cpp
|
|
|
|
Dominic Mazzoni
|
|
and lots of other contributors
|
|
|
|
Implements TrackPanel and TrackInfo.
|
|
|
|
********************************************************************//*!
|
|
|
|
\todo
|
|
Refactoring of the TrackPanel, possibly as described
|
|
in \ref TrackPanelRefactor
|
|
|
|
*//*****************************************************************//*!
|
|
|
|
\file TrackPanel.cpp
|
|
\brief
|
|
Implements TrackPanel and TrackInfo.
|
|
|
|
TrackPanel.cpp is currently some of the worst code in Audacity.
|
|
It's not really unreadable, there's just way too much stuff in this
|
|
one file. Rather than apply a quick fix, the long-term plan
|
|
is to create a GUITrack class that knows how to draw itself
|
|
and handle events. Then this class just helps coordinate
|
|
between tracks.
|
|
|
|
Plans under discussion are described in \ref TrackPanelRefactor
|
|
|
|
*//********************************************************************/
|
|
|
|
// Documentation: Rather than have a lengthy \todo section, having
|
|
// a \todo a \file and a \page in EXACTLY that order gets Doxygen to
|
|
// put the following lengthy description of refactoring on a new page
|
|
// and link to it from the docs.
|
|
|
|
/*****************************************************************//**
|
|
|
|
\class TrackPanel
|
|
\brief
|
|
The TrackPanel class coordinates updates and operations on the
|
|
main part of the screen which contains multiple tracks.
|
|
|
|
It uses many other classes, but in particular it uses the
|
|
TrackInfo class to draw the controls area on the left of a track,
|
|
and the TrackArtist class to draw the actual waveforms.
|
|
|
|
Note that in some of the older code here, e.g., GetLabelWidth(),
|
|
"Label" means the TrackInfo plus the vertical ruler.
|
|
Confusing relative to LabelTrack labels.
|
|
|
|
The TrackPanel manages multiple tracks and their TrackInfos.
|
|
|
|
Note that with stereo tracks there will be one TrackInfo
|
|
being used by two wavetracks.
|
|
|
|
*//*****************************************************************//**
|
|
|
|
\class TrackInfo
|
|
\brief
|
|
The TrackInfo is shown to the side of a track
|
|
It has the menus, pan and gain controls displayed in it.
|
|
So "Info" is somewhat a misnomer. Should possibly be "TrackControls".
|
|
|
|
TrackPanel and not TrackInfo takes care of the functionality for
|
|
each of the buttons in that panel.
|
|
|
|
In its current implementation TrackInfo is not derived from a
|
|
wxWindow. Following the original coding style, it has
|
|
been coded as a 'flyweight' class, which is passed
|
|
state as needed, except for the array of gains and pans.
|
|
|
|
If we'd instead coded it as a wxWindow, we would have an instance
|
|
of this class for each instance displayed.
|
|
|
|
*//**************************************************************//**
|
|
|
|
\class TrackClip
|
|
\brief One clip (i.e short section) of a WaveTrack.
|
|
|
|
*//**************************************************************//**
|
|
|
|
\class TrackPanelListener
|
|
\brief A now badly named class which is used to give access to a
|
|
subset of the TrackPanel methods from all over the place.
|
|
|
|
*//**************************************************************//**
|
|
|
|
\class TrackList
|
|
\brief A list of TrackListNode items.
|
|
|
|
*//**************************************************************//**
|
|
|
|
\class TrackListIterator
|
|
\brief An iterator for a TrackList.
|
|
|
|
*//**************************************************************//**
|
|
|
|
\class TrackListNode
|
|
\brief Used by TrackList, points to a Track.
|
|
|
|
*//**************************************************************//**
|
|
|
|
\class TrackPanel::AudacityTimer
|
|
\brief Timer class dedicated to infomring the TrackPanel that it
|
|
is time to refresh some aspect of the screen.
|
|
|
|
*//*****************************************************************//**
|
|
|
|
\page TrackPanelRefactor Track Panel Refactor
|
|
\brief Planned refactoring of TrackPanel.cpp
|
|
|
|
- Move menus from current TrackPanel into TrackInfo.
|
|
- Convert TrackInfo from 'flyweight' to heavyweight.
|
|
- Split GuiStereoTrack and GuiWaveTrack out from TrackPanel.
|
|
|
|
JKC: Incremental refactoring started April/2003
|
|
|
|
Possibly aiming for Gui classes something like this - it's under
|
|
discussion:
|
|
|
|
<pre>
|
|
+----------------------------------------------------+
|
|
| AdornedRulerPanel |
|
|
+----------------------------------------------------+
|
|
+----------------------------------------------------+
|
|
|+------------+ +-----------------------------------+|
|
|
|| | | (L) GuiWaveTrack ||
|
|
|| TrackInfo | +-----------------------------------+|
|
|
|| | +-----------------------------------+|
|
|
|| | | (R) GuiWaveTrack ||
|
|
|+------------+ +-----------------------------------+|
|
|
+-------- GuiStereoTrack ----------------------------+
|
|
+----------------------------------------------------+
|
|
|+------------+ +-----------------------------------+|
|
|
|| | | (L) GuiWaveTrack ||
|
|
|| TrackInfo | +-----------------------------------+|
|
|
|| | +-----------------------------------+|
|
|
|| | | (R) GuiWaveTrack ||
|
|
|+------------+ +-----------------------------------+|
|
|
+-------- GuiStereoTrack ----------------------------+
|
|
</pre>
|
|
|
|
With the whole lot sitting in a TrackPanel which forwards
|
|
events to the sub objects.
|
|
|
|
The GuiStereoTrack class will do the special logic for
|
|
Stereo channel grouping.
|
|
|
|
The precise names of the classes are subject to revision.
|
|
Have deliberately not created new files for the new classes
|
|
such as AdornedRulerPanel and TrackInfo - yet.
|
|
|
|
*//*****************************************************************/
|
|
|
|
|
|
#include "Audacity.h"
|
|
#include "Experimental.h"
|
|
#include "TrackPanel.h"
|
|
|
|
#include <math.h>
|
|
#include <stdlib.h>
|
|
|
|
//#define DEBUG_DRAW_TIMING 1
|
|
// #define SPECTRAL_EDITING_ESC_KEY
|
|
|
|
#include <wx/combobox.h>
|
|
#include <wx/dcclient.h>
|
|
#include <wx/dcbuffer.h>
|
|
#include <wx/dcmemory.h>
|
|
#include <wx/font.h>
|
|
#include <wx/fontenum.h>
|
|
#include <wx/log.h>
|
|
#include <wx/menu.h>
|
|
#include <wx/msgdlg.h>
|
|
#include <wx/textdlg.h>
|
|
#include <wx/numdlg.h>
|
|
#include <wx/choicdlg.h>
|
|
#include <wx/spinctrl.h>
|
|
#include <wx/listbox.h>
|
|
#include <wx/textctrl.h>
|
|
#include <wx/intl.h>
|
|
#include <wx/image.h>
|
|
|
|
#include "FreqWindow.h" // for SpectrumAnalyst
|
|
|
|
#include "AColor.h"
|
|
#include "AllThemeResources.h"
|
|
#include "AudacityApp.h"
|
|
#include "AudioIO.h"
|
|
#include "Envelope.h"
|
|
#include "Experimental.h"
|
|
#include "float_cast.h"
|
|
#include "Internat.h"
|
|
#include "LabelTrack.h"
|
|
#include "MixerBoard.h"
|
|
|
|
#include "NoteTrack.h"
|
|
#include "NumberScale.h"
|
|
#include "Prefs.h"
|
|
#include "Project.h"
|
|
#include "Snap.h"
|
|
#include "ShuttleGui.h"
|
|
#include "Theme.h"
|
|
#include "TimeTrack.h"
|
|
#include "TrackArtist.h"
|
|
#include "TrackPanelAx.h"
|
|
#include "ViewInfo.h"
|
|
#include "WaveTrack.h"
|
|
|
|
#include "commands/Keyboard.h"
|
|
|
|
#include "ondemand/ODManager.h"
|
|
|
|
#include "prefs/PrefsDialog.h"
|
|
#include "prefs/SpectrumPrefs.h"
|
|
#include "prefs/WaveformPrefs.h"
|
|
|
|
#include "toolbars/ControlToolBar.h"
|
|
#include "toolbars/ToolManager.h"
|
|
#include "toolbars/ToolsToolBar.h"
|
|
// #include "toolbars/TranscriptionToolBar.h"
|
|
|
|
#include "widgets/ASlider.h"
|
|
#include "widgets/Ruler.h"
|
|
#include "widgets/NumericTextCtrl.h"
|
|
|
|
#define ZOOMLIMIT 0.001f
|
|
|
|
//This loads the appropriate set of cursors, depending on platform.
|
|
#include "../images/Cursors.h"
|
|
#include <iostream>
|
|
|
|
DEFINE_EVENT_TYPE(EVT_TRACK_PANEL_TIMER)
|
|
|
|
enum {
|
|
kLeftInset = 4,
|
|
kTopInset = 4,
|
|
kTimerInterval = 50, // milliseconds
|
|
kOneSecondCountdown = 1000 / kTimerInterval,
|
|
};
|
|
|
|
enum {
|
|
// PRL:
|
|
// Mouse must move at least this far to distinguish ctrl-drag to scrub
|
|
// from ctrl-click for playback.
|
|
SCRUBBING_PIXEL_TOLERANCE = 10,
|
|
|
|
#ifdef EXPERIMENTAL_SCRUBBING_SCROLL_WHEEL
|
|
ScrubSpeedStepsPerOctave = 4,
|
|
#endif
|
|
};
|
|
|
|
// Is the distance between A and B less than D?
|
|
template < class A, class B, class DIST > bool within(A a, B b, DIST d)
|
|
{
|
|
return (a > b - d) && (a < b + d);
|
|
}
|
|
|
|
template < class LOW, class MID, class HIGH >
|
|
bool between_inclusive(LOW l, MID m, HIGH h)
|
|
{
|
|
return (m >= l && m <= h);
|
|
}
|
|
|
|
template < class LOW, class MID, class HIGH >
|
|
bool between_exclusive(LOW l, MID m, HIGH h)
|
|
{
|
|
return (m > l && m < h);
|
|
}
|
|
|
|
template < class CLIPPEE, class CLIPVAL >
|
|
void clip_top(CLIPPEE & clippee, CLIPVAL val)
|
|
{
|
|
if (clippee > val)
|
|
clippee = val;
|
|
}
|
|
|
|
template < class CLIPPEE, class CLIPVAL >
|
|
void clip_bottom(CLIPPEE & clippee, CLIPVAL val)
|
|
{
|
|
if (clippee < val)
|
|
clippee = val;
|
|
}
|
|
|
|
enum {
|
|
TrackPanelFirstID = 2000,
|
|
OnSetNameID,
|
|
OnSetFontID,
|
|
|
|
OnMoveUpID,
|
|
OnMoveDownID,
|
|
OnMoveTopID,
|
|
OnMoveBottomID,
|
|
|
|
OnUpOctaveID,
|
|
OnDownOctaveID,
|
|
|
|
OnChannelLeftID,
|
|
OnChannelRightID,
|
|
OnChannelMonoID,
|
|
|
|
OnRate8ID, // <---
|
|
OnRate11ID, // |
|
|
OnRate16ID, // |
|
|
OnRate22ID, // |
|
|
OnRate44ID, // |
|
|
OnRate48ID, // | Leave these in order
|
|
OnRate88ID, // |
|
|
OnRate96ID, // |
|
|
OnRate176ID, // | see OnTrackMenu()
|
|
OnRate192ID, // |
|
|
OnRate352ID, // |
|
|
OnRate384ID, // |
|
|
OnRateOtherID, // |
|
|
// |
|
|
On16BitID, // |
|
|
On24BitID, // |
|
|
OnFloatID, // <---
|
|
|
|
OnWaveformID,
|
|
OnSpectrumID,
|
|
OnViewSettingsID,
|
|
|
|
OnSplitStereoID,
|
|
OnSplitStereoMonoID,
|
|
OnMergeStereoID,
|
|
OnSwapChannelsID,
|
|
|
|
OnSetTimeTrackRangeID,
|
|
OnCutSelectedTextID,
|
|
OnCopySelectedTextID,
|
|
OnPasteSelectedTextID,
|
|
OnDeleteSelectedLabelID,
|
|
|
|
OnTimeTrackLinID,
|
|
OnTimeTrackLogID,
|
|
OnTimeTrackLogIntID,
|
|
|
|
// Reserve an ample block of ids for waveform scale types
|
|
OnFirstWaveformScaleID,
|
|
OnLastWaveformScaleID = OnFirstWaveformScaleID + 9,
|
|
|
|
// Reserve an ample block of ids for spectrum scale types
|
|
OnFirstSpectrumScaleID,
|
|
OnLastSpectrumScaleID = OnFirstSpectrumScaleID + 19,
|
|
|
|
OnZoomInVerticalID,
|
|
OnZoomOutVerticalID,
|
|
OnZoomFitVerticalID,
|
|
};
|
|
|
|
BEGIN_EVENT_TABLE(TrackPanel, wxWindow)
|
|
EVT_MOUSE_EVENTS(TrackPanel::OnMouseEvent)
|
|
EVT_MOUSE_CAPTURE_LOST(TrackPanel::OnCaptureLost)
|
|
EVT_COMMAND(wxID_ANY, EVT_CAPTURE_KEY, TrackPanel::OnCaptureKey)
|
|
EVT_KEY_DOWN(TrackPanel::OnKeyDown)
|
|
EVT_CHAR(TrackPanel::OnChar)
|
|
EVT_SIZE(TrackPanel::OnSize)
|
|
EVT_ERASE_BACKGROUND(TrackPanel::OnErase)
|
|
EVT_PAINT(TrackPanel::OnPaint)
|
|
EVT_SET_FOCUS(TrackPanel::OnSetFocus)
|
|
EVT_KILL_FOCUS(TrackPanel::OnKillFocus)
|
|
EVT_CONTEXT_MENU(TrackPanel::OnContextMenu)
|
|
EVT_MENU(OnSetNameID, TrackPanel::OnSetName)
|
|
EVT_MENU(OnSetFontID, TrackPanel::OnSetFont)
|
|
EVT_MENU(OnSetTimeTrackRangeID, TrackPanel::OnSetTimeTrackRange)
|
|
|
|
EVT_MENU_RANGE(OnMoveUpID, OnMoveDownID, TrackPanel::OnMoveTrack)
|
|
EVT_MENU_RANGE(OnMoveTopID, OnMoveBottomID, TrackPanel::OnMoveTrack)
|
|
EVT_MENU_RANGE(OnUpOctaveID, OnDownOctaveID, TrackPanel::OnChangeOctave)
|
|
EVT_MENU_RANGE(OnChannelLeftID, OnChannelMonoID,
|
|
TrackPanel::OnChannelChange)
|
|
EVT_MENU_RANGE(OnWaveformID, OnSpectrumID, TrackPanel::OnSetDisplay)
|
|
EVT_MENU(OnViewSettingsID, TrackPanel::OnViewSettings)
|
|
EVT_MENU_RANGE(OnRate8ID, OnRate384ID, TrackPanel::OnRateChange)
|
|
EVT_MENU_RANGE(On16BitID, OnFloatID, TrackPanel::OnFormatChange)
|
|
EVT_MENU(OnRateOtherID, TrackPanel::OnRateOther)
|
|
EVT_MENU(OnSwapChannelsID, TrackPanel::OnSwapChannels)
|
|
EVT_MENU(OnSplitStereoID, TrackPanel::OnSplitStereo)
|
|
EVT_MENU(OnSplitStereoMonoID, TrackPanel::OnSplitStereoMono)
|
|
EVT_MENU(OnMergeStereoID, TrackPanel::OnMergeStereo)
|
|
|
|
EVT_MENU(OnCutSelectedTextID, TrackPanel::OnCutSelectedText)
|
|
EVT_MENU(OnCopySelectedTextID, TrackPanel::OnCopySelectedText)
|
|
EVT_MENU(OnPasteSelectedTextID, TrackPanel::OnPasteSelectedText)
|
|
EVT_MENU(OnDeleteSelectedLabelID, TrackPanel::OnDeleteSelectedLabel)
|
|
|
|
EVT_MENU(OnTimeTrackLinID, TrackPanel::OnTimeTrackLin)
|
|
EVT_MENU(OnTimeTrackLogID, TrackPanel::OnTimeTrackLog)
|
|
EVT_MENU(OnTimeTrackLogIntID, TrackPanel::OnTimeTrackLogInt)
|
|
|
|
EVT_MENU_RANGE(OnFirstWaveformScaleID, OnLastWaveformScaleID, TrackPanel::OnWaveformScaleType)
|
|
EVT_MENU_RANGE(OnFirstSpectrumScaleID, OnLastSpectrumScaleID, TrackPanel::OnSpectrumScaleType)
|
|
|
|
EVT_MENU(OnZoomInVerticalID, TrackPanel::OnZoomInVertical)
|
|
EVT_MENU(OnZoomOutVerticalID, TrackPanel::OnZoomOutVertical)
|
|
EVT_MENU(OnZoomFitVerticalID, TrackPanel::OnZoomFitVertical)
|
|
END_EVENT_TABLE()
|
|
|
|
/// Makes a cursor from an XPM, uses CursorId as a fallback.
|
|
/// TODO: Move this function to some other source file for reuse elsewhere.
|
|
wxCursor * MakeCursor( int WXUNUSED(CursorId), const char * pXpm[36], int HotX, int HotY )
|
|
{
|
|
wxCursor * pCursor;
|
|
|
|
#ifdef CURSORS_SIZE32
|
|
const int HotAdjust =0;
|
|
#else
|
|
const int HotAdjust =8;
|
|
#endif
|
|
|
|
wxImage Image = wxImage(wxBitmap(pXpm).ConvertToImage());
|
|
Image.SetMaskColour(255,0,0);
|
|
Image.SetMask();// Enable mask.
|
|
|
|
#if defined(__WXGTK__) && !wxCHECK_VERSION(3, 0, 0)
|
|
//
|
|
// Kludge: the wxCursor Image constructor is broken in wxGTK.
|
|
// This code, based loosely on the broken code from the wxGTK source,
|
|
// works around the problem by constructing a 1-bit bitmap and
|
|
// calling the other custom cursor constructor.
|
|
//
|
|
// -DMM
|
|
//
|
|
|
|
unsigned char *rgbBits = Image.GetData();
|
|
int w = Image.GetWidth() ;
|
|
int h = Image.GetHeight();
|
|
int imagebitcount = (w*h)/8;
|
|
|
|
unsigned char *bits = new unsigned char [imagebitcount];
|
|
unsigned char *maskBits = new unsigned char [imagebitcount];
|
|
|
|
int i, j, i8;
|
|
unsigned char cMask;
|
|
for (i=0; i<imagebitcount; i++) {
|
|
bits[i] = 0;
|
|
i8 = i * 8;
|
|
|
|
cMask = 1;
|
|
for (j=0; j<8; j++) {
|
|
if (rgbBits[(i8+j)*3+2] < 127)
|
|
bits[i] = bits[i] | cMask;
|
|
cMask = cMask * 2;
|
|
}
|
|
}
|
|
|
|
for (i=0; i<imagebitcount; i++) {
|
|
maskBits[i] = 0x0;
|
|
i8 = i * 8;
|
|
|
|
cMask = 1;
|
|
for (j=0; j<8; j++) {
|
|
if (rgbBits[(i8+j)*3] < 127 || rgbBits[(i8+j)*3+1] > 127)
|
|
maskBits[i] = maskBits[i] | cMask;
|
|
cMask = cMask * 2;
|
|
}
|
|
}
|
|
|
|
pCursor = new wxCursor((const char *)bits, w, h,
|
|
HotX-HotAdjust, HotY-HotAdjust,
|
|
(const char *)maskBits);
|
|
|
|
delete [] bits;
|
|
delete [] maskBits;
|
|
|
|
#else
|
|
Image.SetOption( wxIMAGE_OPTION_CUR_HOTSPOT_X, HotX-HotAdjust );
|
|
Image.SetOption( wxIMAGE_OPTION_CUR_HOTSPOT_Y, HotY-HotAdjust );
|
|
pCursor = new wxCursor( Image );
|
|
#endif
|
|
|
|
return pCursor;
|
|
}
|
|
|
|
|
|
|
|
// Don't warn us about using 'this' in the base member initializer list.
|
|
#ifndef __WXGTK__ //Get rid if this pragma for gtk
|
|
#pragma warning( disable: 4355 )
|
|
#endif
|
|
TrackPanel::TrackPanel(wxWindow * parent, wxWindowID id,
|
|
const wxPoint & pos,
|
|
const wxSize & size,
|
|
TrackList * tracks,
|
|
ViewInfo * viewInfo,
|
|
TrackPanelListener * listener,
|
|
AdornedRulerPanel * ruler)
|
|
: wxPanel(parent, id, pos, size, wxWANTS_CHARS | wxNO_BORDER),
|
|
mTrackInfo(this),
|
|
mListener(listener),
|
|
mTracks(tracks),
|
|
mViewInfo(viewInfo),
|
|
mRuler(ruler),
|
|
mTrackArtist(NULL),
|
|
mBacking(NULL),
|
|
mRefreshBacking(false),
|
|
mConverter(NumericConverter::TIME),
|
|
mAutoScrolling(false),
|
|
mVertScrollRemainder(0),
|
|
vrulerSize(36,0)
|
|
#ifndef __WXGTK__ //Get rid if this pragma for gtk
|
|
#pragma warning( default: 4355 )
|
|
#endif
|
|
{
|
|
SetLabel(_("Track Panel"));
|
|
SetName(_("Track Panel"));
|
|
|
|
mAx = new TrackPanelAx( this );
|
|
#if wxUSE_ACCESSIBILITY
|
|
SetAccessible( mAx );
|
|
#endif
|
|
mMouseCapture = IsUncaptured;
|
|
mSlideUpDownOnly = false;
|
|
mLabelTrackStartXPos=-1;
|
|
mCircularTrackNavigation = false;
|
|
|
|
UpdatePrefs();
|
|
|
|
mRedrawAfterStop = false;
|
|
mIndicatorShowing = false;
|
|
|
|
mPencilCursor = MakeCursor( wxCURSOR_PENCIL, DrawCursorXpm, 12, 22);
|
|
mSelectCursor = MakeCursor( wxCURSOR_IBEAM, IBeamCursorXpm, 17, 16);
|
|
mEnvelopeCursor= MakeCursor( wxCURSOR_ARROW, EnvCursorXpm, 16, 16);
|
|
mDisabledCursor= MakeCursor( wxCURSOR_NO_ENTRY, DisabledCursorXpm,16, 16);
|
|
mSlideCursor = MakeCursor( wxCURSOR_SIZEWE, TimeCursorXpm, 16, 16);
|
|
mZoomInCursor = MakeCursor( wxCURSOR_MAGNIFIER, ZoomInCursorXpm, 19, 15);
|
|
mZoomOutCursor = MakeCursor( wxCURSOR_MAGNIFIER, ZoomOutCursorXpm, 19, 15);
|
|
mLabelCursorLeft = MakeCursor( wxCURSOR_ARROW, LabelCursorLeftXpm, 19, 15);
|
|
mLabelCursorRight = MakeCursor( wxCURSOR_ARROW, LabelCursorRightXpm, 16, 16);
|
|
|
|
#ifdef EXPERIMENTAL_SPECTRAL_EDITING
|
|
mBottomFrequencyCursor =
|
|
MakeCursor( wxCURSOR_ARROW, BottomFrequencyCursorXpm, 16, 16);
|
|
mTopFrequencyCursor =
|
|
MakeCursor( wxCURSOR_ARROW, TopFrequencyCursorXpm, 16, 16);
|
|
mBandWidthCursor = MakeCursor( wxCURSOR_ARROW, BandWidthCursorXpm, 16, 16);
|
|
#endif
|
|
|
|
#if USE_MIDI
|
|
mStretchMode = stretchCenter;
|
|
mStretching = false;
|
|
mStretched = false;
|
|
mStretchStart = 0;
|
|
mStretchCursor = MakeCursor( wxCURSOR_BULLSEYE, StretchCursorXpm, 16, 16);
|
|
mStretchLeftCursor = MakeCursor( wxCURSOR_BULLSEYE,
|
|
StretchLeftCursorXpm, 16, 16);
|
|
mStretchRightCursor = MakeCursor( wxCURSOR_BULLSEYE,
|
|
StretchRightCursorXpm, 16, 16);
|
|
#endif
|
|
|
|
mArrowCursor = new wxCursor(wxCURSOR_ARROW);
|
|
mSmoothCursor = new wxCursor(wxCURSOR_SPRAYCAN);
|
|
mResizeCursor = new wxCursor(wxCURSOR_SIZENS);
|
|
mRearrangeCursor = new wxCursor(wxCURSOR_HAND);
|
|
mAdjustLeftSelectionCursor = new wxCursor(wxCURSOR_POINT_LEFT);
|
|
mAdjustRightSelectionCursor = new wxCursor(wxCURSOR_POINT_RIGHT);
|
|
|
|
mWaveTrackMenu = NULL;
|
|
mNoteTrackMenu = NULL;
|
|
mLabelTrackMenu = NULL;
|
|
mLabelTrackInfoMenu = NULL;
|
|
mTimeTrackMenu = NULL;
|
|
|
|
mRulerWaveformMenu = mRulerSpectrumMenu = NULL;
|
|
|
|
BuildMenus();
|
|
|
|
mTrackArtist = new TrackArtist();
|
|
mTrackArtist->SetInset(1, kTopInset + 1, kLeftInset + 2, 2);
|
|
|
|
mCapturedTrack = NULL;
|
|
mPopupMenuTarget = NULL;
|
|
|
|
mTimeCount = 0;
|
|
mTimer.parent = this;
|
|
mTimer.Start(kTimerInterval, FALSE);
|
|
|
|
//Initialize a member variable pointing to the current
|
|
//drawing track.
|
|
mDrawingTrack =NULL;
|
|
|
|
//More initializations, some of these for no other reason than
|
|
//to prevent runtime memory check warnings
|
|
mZoomStart = -1;
|
|
mZoomEnd = -1;
|
|
mPrevWidth = -1;
|
|
mPrevHeight = -1;
|
|
|
|
//Initialize the last selection adjustment time.
|
|
mLastSelectionAdjustment = ::wxGetLocalTimeMillis();
|
|
|
|
// This is used to snap the cursor to the nearest track that
|
|
// lines up with it.
|
|
mSnapManager = NULL;
|
|
mSnapLeft = -1;
|
|
mSnapRight = -1;
|
|
|
|
mLastCursorX = -1;
|
|
mLastIndicatorX = -1;
|
|
mOldQPIndicatorPos = -1;
|
|
|
|
// Register for tracklist updates
|
|
mTracks->Connect(EVT_TRACKLIST_RESIZED,
|
|
wxCommandEventHandler(TrackPanel::OnTrackListResized),
|
|
NULL,
|
|
this);
|
|
mTracks->Connect(EVT_TRACKLIST_UPDATED,
|
|
wxCommandEventHandler(TrackPanel::OnTrackListUpdated),
|
|
NULL,
|
|
this);
|
|
|
|
#ifdef EXPERIMENTAL_SPECTRAL_EDITING
|
|
mFreqSelMode = FREQ_SEL_INVALID;
|
|
mFrequencySnapper.reset(new SpectrumAnalyst());
|
|
|
|
mLastF0 = mLastF1 = SelectedRegion::UndefinedFrequency;
|
|
#endif
|
|
|
|
mSelStartValid = false;
|
|
mSelStart = 0;
|
|
|
|
#ifdef EXPERIMENTAL_SCRUBBING_BASIC
|
|
mScrubToken = -1;
|
|
mScrubStartClockTimeMillis = -1;
|
|
mScrubStartPosition = -1;
|
|
mMaxScrubSpeed = 1.0;
|
|
mScrubSpeedDisplayCountdown = 0;
|
|
mScrubHasFocus = false;
|
|
mScrubSeekPress = false;
|
|
#endif
|
|
|
|
#ifdef EXPERIMENTAL_SCRUBBING_SMOOTH_SCROLL
|
|
mSmoothScrollingScrub = false;
|
|
#endif
|
|
|
|
#ifdef EXPERIMENTAL_SCRUBBING_SCROLL_WHEEL
|
|
mLogMaxScrubSpeed = 0;
|
|
#endif
|
|
|
|
mInitialTrackSelection = new std::vector<bool>;
|
|
}
|
|
|
|
TrackPanel::~TrackPanel()
|
|
{
|
|
mTimer.Stop();
|
|
|
|
// Unregister for tracklist updates
|
|
mTracks->Disconnect(EVT_TRACKLIST_UPDATED,
|
|
wxCommandEventHandler(TrackPanel::OnTrackListUpdated),
|
|
NULL,
|
|
this);
|
|
mTracks->Disconnect(EVT_TRACKLIST_RESIZED,
|
|
wxCommandEventHandler(TrackPanel::OnTrackListResized),
|
|
NULL,
|
|
this);
|
|
|
|
// This can happen if a label is being edited and the user presses
|
|
// ALT+F4 or Command+Q
|
|
if (HasCapture())
|
|
ReleaseMouse();
|
|
|
|
if (mBacking)
|
|
{
|
|
mBackingDC.SelectObject( wxNullBitmap );
|
|
delete mBacking;
|
|
}
|
|
delete mTrackArtist;
|
|
|
|
delete mArrowCursor;
|
|
delete mPencilCursor;
|
|
delete mSelectCursor;
|
|
delete mEnvelopeCursor;
|
|
delete mDisabledCursor;
|
|
delete mSlideCursor;
|
|
delete mResizeCursor;
|
|
delete mSmoothCursor;
|
|
delete mZoomInCursor;
|
|
delete mZoomOutCursor;
|
|
delete mLabelCursorLeft;
|
|
delete mLabelCursorRight;
|
|
delete mRearrangeCursor;
|
|
delete mAdjustLeftSelectionCursor;
|
|
delete mAdjustRightSelectionCursor;
|
|
#ifdef EXPERIMENTAL_SPECTRAL_EDITING
|
|
delete mBottomFrequencyCursor;
|
|
delete mTopFrequencyCursor;
|
|
delete mBandWidthCursor;
|
|
#endif
|
|
#if USE_MIDI
|
|
delete mStretchCursor;
|
|
delete mStretchLeftCursor;
|
|
delete mStretchRightCursor;
|
|
#endif
|
|
|
|
delete mSnapManager;
|
|
|
|
DeleteMenus();
|
|
|
|
#if !wxUSE_ACCESSIBILITY
|
|
delete mAx;
|
|
#endif
|
|
|
|
delete mInitialTrackSelection;
|
|
}
|
|
|
|
void TrackPanel::BuildMenus(void)
|
|
{
|
|
// Get rid of existing menus
|
|
DeleteMenus();
|
|
|
|
// Use AppendCheckItem so we can have ticks beside the items.
|
|
// We would use AppendRadioItem but it only currently works on windows and GTK.
|
|
mRateMenu = new wxMenu();
|
|
mRateMenu->AppendRadioItem(OnRate8ID, wxT("8000 Hz"));
|
|
mRateMenu->AppendRadioItem(OnRate11ID, wxT("11025 Hz"));
|
|
mRateMenu->AppendRadioItem(OnRate16ID, wxT("16000 Hz"));
|
|
mRateMenu->AppendRadioItem(OnRate22ID, wxT("22050 Hz"));
|
|
mRateMenu->AppendRadioItem(OnRate44ID, wxT("44100 Hz"));
|
|
mRateMenu->AppendRadioItem(OnRate48ID, wxT("48000 Hz"));
|
|
mRateMenu->AppendRadioItem(OnRate88ID, wxT("88200 Hz"));
|
|
mRateMenu->AppendRadioItem(OnRate96ID, wxT("96000 Hz"));
|
|
mRateMenu->AppendRadioItem(OnRate176ID, wxT("176400 Hz"));
|
|
mRateMenu->AppendRadioItem(OnRate192ID, wxT("192000 Hz"));
|
|
mRateMenu->AppendRadioItem(OnRate352ID, wxT("352800 Hz"));
|
|
mRateMenu->AppendRadioItem(OnRate384ID, wxT("384000 Hz"));
|
|
mRateMenu->AppendRadioItem(OnRateOtherID, _("&Other..."));
|
|
|
|
mFormatMenu = new wxMenu();
|
|
mFormatMenu->AppendRadioItem(On16BitID, GetSampleFormatStr(int16Sample));
|
|
mFormatMenu->AppendRadioItem(On24BitID, GetSampleFormatStr(int24Sample));
|
|
mFormatMenu->AppendRadioItem(OnFloatID, GetSampleFormatStr(floatSample));
|
|
|
|
/* build the pop-down menu used on wave (sampled audio) tracks */
|
|
mWaveTrackMenu = new wxMenu();
|
|
BuildCommonDropMenuItems(mWaveTrackMenu); // does name, up/down etc
|
|
mWaveTrackMenu->AppendRadioItem(OnWaveformID, _("&Waveform"));
|
|
mWaveTrackMenu->AppendRadioItem(OnSpectrumID, _("&Spectrum"));
|
|
mWaveTrackMenu->Append(OnViewSettingsID, _("&View Settings..."));
|
|
mWaveTrackMenu->AppendSeparator();
|
|
|
|
mWaveTrackMenu->AppendRadioItem(OnChannelMonoID, _("&Mono"));
|
|
mWaveTrackMenu->AppendRadioItem(OnChannelLeftID, _("&Left Channel"));
|
|
mWaveTrackMenu->AppendRadioItem(OnChannelRightID, _("&Right Channel"));
|
|
mWaveTrackMenu->Append(OnMergeStereoID, _("Ma&ke Stereo Track"));
|
|
mWaveTrackMenu->Append(OnSwapChannelsID, _("Swap Stereo &Channels"));
|
|
mWaveTrackMenu->Append(OnSplitStereoID, _("Spl&it Stereo Track"));
|
|
mWaveTrackMenu->Append(OnSplitStereoMonoID, _("Split Stereo to Mo&no"));
|
|
mWaveTrackMenu->AppendSeparator();
|
|
mWaveTrackMenu->Append(0, _("Set Sample &Format"), mFormatMenu);
|
|
mWaveTrackMenu->AppendSeparator();
|
|
mWaveTrackMenu->Append(0, _("Set Rat&e"), mRateMenu);
|
|
|
|
/* build the pop-down menu used on note (MIDI) tracks */
|
|
mNoteTrackMenu = new wxMenu();
|
|
BuildCommonDropMenuItems(mNoteTrackMenu); // does name, up/down etc
|
|
mNoteTrackMenu->Append(OnUpOctaveID, _("Up &Octave"));
|
|
mNoteTrackMenu->Append(OnDownOctaveID, _("Down Octa&ve"));
|
|
|
|
/* build the pop-down menu used on label tracks */
|
|
mLabelTrackMenu = new wxMenu();
|
|
BuildCommonDropMenuItems(mLabelTrackMenu); // does name, up/down etc
|
|
mLabelTrackMenu->Append(OnSetFontID, _("&Font..."));
|
|
|
|
/* build the pop-down menu used on time warping tracks */
|
|
mTimeTrackMenu = new wxMenu();
|
|
BuildCommonDropMenuItems(mTimeTrackMenu); // does name, up/down etc
|
|
mTimeTrackMenu->Append(OnTimeTrackLinID, _("&Linear"));
|
|
mTimeTrackMenu->Append(OnTimeTrackLogID, _("L&ogarithmic"));
|
|
mTimeTrackMenu->AppendSeparator();
|
|
mTimeTrackMenu->Append(OnSetTimeTrackRangeID, _("Set Ra&nge..."));
|
|
mTimeTrackMenu->AppendCheckItem(OnTimeTrackLogIntID, _("Logarithmic &Interpolation"));
|
|
|
|
mLabelTrackInfoMenu = new wxMenu();
|
|
mLabelTrackInfoMenu->Append(OnCutSelectedTextID, _("Cu&t"));
|
|
mLabelTrackInfoMenu->Append(OnCopySelectedTextID, _("&Copy"));
|
|
mLabelTrackInfoMenu->Append(OnPasteSelectedTextID, _("&Paste"));
|
|
mLabelTrackInfoMenu->Append(OnDeleteSelectedLabelID, _("&Delete Label"));
|
|
|
|
mRulerWaveformMenu = new wxMenu();
|
|
BuildVRulerMenuItems
|
|
(mRulerWaveformMenu, OnFirstWaveformScaleID,
|
|
WaveformSettings::GetScaleNames());
|
|
|
|
mRulerSpectrumMenu = new wxMenu();
|
|
BuildVRulerMenuItems
|
|
(mRulerSpectrumMenu, OnFirstSpectrumScaleID,
|
|
SpectrogramSettings::GetScaleNames());
|
|
}
|
|
|
|
void TrackPanel::BuildCommonDropMenuItems(wxMenu * menu)
|
|
{
|
|
menu->Append(OnSetNameID, _("N&ame..."));
|
|
menu->AppendSeparator();
|
|
menu->Append(OnMoveUpID, _("Move Track &Up"));
|
|
menu->Append(OnMoveDownID, _("Move Track &Down"));
|
|
menu->Append(OnMoveTopID, _("Move Track to &Top"));
|
|
menu->Append(OnMoveBottomID, _("Move Track to &Bottom"));
|
|
menu->AppendSeparator();
|
|
|
|
}
|
|
|
|
// static
|
|
void TrackPanel::BuildVRulerMenuItems
|
|
(wxMenu * menu, int firstId, const wxArrayString &names)
|
|
{
|
|
int id = firstId;
|
|
for (int ii = 0, nn = names.size(); ii < nn; ++ii)
|
|
menu->AppendRadioItem(id++, names[ii]);
|
|
menu->AppendSeparator();
|
|
menu->Append(OnZoomInVerticalID, _("Zoom In\tLeft-Click/Left-Drag"));
|
|
menu->Append(OnZoomOutVerticalID, _("Zoom Out\tShift-Left-Click"));
|
|
menu->Append(OnZoomFitVerticalID, _("Zoom to Fit\tShift-Right-Click"));
|
|
}
|
|
|
|
void TrackPanel::DeleteMenus(void)
|
|
{
|
|
// Note that the submenus (mRateMenu, ...)
|
|
// are deleted by their parent
|
|
if (mWaveTrackMenu) {
|
|
delete mWaveTrackMenu;
|
|
mWaveTrackMenu = NULL;
|
|
}
|
|
|
|
if (mNoteTrackMenu) {
|
|
delete mNoteTrackMenu;
|
|
mNoteTrackMenu = NULL;
|
|
}
|
|
|
|
if (mLabelTrackMenu) {
|
|
delete mLabelTrackMenu;
|
|
mLabelTrackMenu = NULL;
|
|
}
|
|
|
|
if (mLabelTrackInfoMenu) {
|
|
delete mLabelTrackInfoMenu;
|
|
mLabelTrackInfoMenu = NULL;
|
|
}
|
|
|
|
if (mTimeTrackMenu) {
|
|
delete mTimeTrackMenu;
|
|
mTimeTrackMenu = NULL;
|
|
}
|
|
|
|
delete mRulerWaveformMenu;
|
|
delete mRulerSpectrumMenu;
|
|
}
|
|
|
|
#ifdef EXPERIMENTAL_OUTPUT_DISPLAY
|
|
void TrackPanel::UpdateVirtualStereoOrder()
|
|
{
|
|
TrackListIterator iter(mTracks);
|
|
Track *t;
|
|
int temp;
|
|
|
|
for (t = iter.First(); t; t = iter.Next()) {
|
|
if(t->GetKind() == Track::Wave && t->GetChannel() == Track::MonoChannel){
|
|
WaveTrack *wt = (WaveTrack*)t;
|
|
|
|
if(WaveTrack::mMonoAsVirtualStereo && wt->GetPan() != 0){
|
|
temp = wt->GetHeight();
|
|
wt->SetHeight(temp*wt->GetVirtualTrackPercentage());
|
|
wt->SetHeight(temp - wt->GetHeight(),true);
|
|
}else if(!WaveTrack::mMonoAsVirtualStereo && wt->GetPan() != 0){
|
|
wt->SetHeight(wt->GetHeight() + wt->GetHeight(true));
|
|
}
|
|
}
|
|
}
|
|
t = iter.First();
|
|
if(t){
|
|
t->ReorderList(false);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
void TrackPanel::UpdatePrefs()
|
|
{
|
|
#ifdef EXPERIMENTAL_SCROLLING_LIMITS
|
|
gPrefs->Read(wxT("/GUI/ScrollBeyondZero"), &mScrollBeyondZero, false);
|
|
#endif
|
|
gPrefs->Read(wxT("/GUI/AutoScroll"), &mViewInfo->bUpdateTrackIndicator,
|
|
true);
|
|
gPrefs->Read(wxT("/GUI/AdjustSelectionEdges"), &mAdjustSelectionEdges,
|
|
true);
|
|
gPrefs->Read(wxT("/GUI/CircularTrackNavigation"), &mCircularTrackNavigation,
|
|
false);
|
|
gPrefs->Read(wxT("/GUI/Solo"), &mSoloPref, wxT("Standard") );
|
|
gPrefs->Read(wxT("/AudioIO/SeekShortPeriod"), &mSeekShort,
|
|
1.0);
|
|
gPrefs->Read(wxT("/AudioIO/SeekLongPeriod"), &mSeekLong,
|
|
15.0);
|
|
|
|
#ifdef EXPERIMENTAL_OUTPUT_DISPLAY
|
|
bool temp = WaveTrack::mMonoAsVirtualStereo;
|
|
gPrefs->Read(wxT("/GUI/MonoAsVirtualStereo"), &WaveTrack::mMonoAsVirtualStereo,
|
|
false);
|
|
|
|
if(WaveTrack::mMonoAsVirtualStereo != temp)
|
|
UpdateVirtualStereoOrder();
|
|
#endif
|
|
|
|
mViewInfo->UpdatePrefs();
|
|
|
|
if (mTrackArtist) {
|
|
mTrackArtist->UpdatePrefs();
|
|
}
|
|
|
|
// All vertical rulers must be recalculated since the minimum and maximum
|
|
// frequences may have been changed.
|
|
UpdateVRulers();
|
|
Refresh();
|
|
}
|
|
|
|
/// Remembers the track we clicked on and why we captured it.
|
|
/// We also use this method to clear the record
|
|
/// of the captured track, passing NULL as the track.
|
|
void TrackPanel::SetCapturedTrack( Track * t, enum MouseCaptureEnum MouseCapture )
|
|
{
|
|
#if defined(__WXDEBUG__)
|
|
if (t == NULL) {
|
|
wxASSERT(MouseCapture == IsUncaptured);
|
|
}
|
|
else {
|
|
wxASSERT(MouseCapture != IsUncaptured);
|
|
}
|
|
#endif
|
|
mCapturedTrack = t;
|
|
mMouseCapture = MouseCapture;
|
|
}
|
|
|
|
void TrackPanel::SelectNone()
|
|
{
|
|
TrackListIterator iter(mTracks);
|
|
Track *t = iter.First();
|
|
while (t) {
|
|
t->SetSelected(false);
|
|
if (t->GetKind() == Track::Label)
|
|
((LabelTrack *) t)->Unselect();
|
|
t = iter.Next();
|
|
}
|
|
}
|
|
|
|
/// Select all tracks marked by the label track lt
|
|
void TrackPanel::SelectTracksByLabel( LabelTrack *lt )
|
|
{
|
|
TrackListIterator iter(mTracks);
|
|
Track *t = iter.First();
|
|
|
|
//do nothing if at least one other track is selected
|
|
while (t) {
|
|
if( t->GetSelected() && t != lt )
|
|
return;
|
|
t = iter.Next();
|
|
}
|
|
|
|
//otherwise, select all tracks
|
|
t = iter.First();
|
|
while( t )
|
|
{
|
|
t->SetSelected( true );
|
|
t = iter.Next();
|
|
}
|
|
}
|
|
|
|
// Set selection length to the length of a track -- but if sync-lock is turned
|
|
// on, use the largest possible selection in the sync-lock group.
|
|
// If it's a stereo track, do the same for the stereo channels.
|
|
void TrackPanel::SelectTrackLength(Track *t)
|
|
{
|
|
AudacityProject *p = GetActiveProject();
|
|
SyncLockedTracksIterator it(mTracks);
|
|
Track *t1 = it.First(t);
|
|
double minOffset = t->GetOffset();
|
|
double maxEnd = t->GetEndTime();
|
|
|
|
// If we have a sync-lock group and sync-lock linking is on,
|
|
// check the sync-lock group tracks.
|
|
if (p->IsSyncLocked() && t1 != NULL)
|
|
{
|
|
for ( ; t1; t1 = it.Next())
|
|
{
|
|
if (t1->GetOffset() < minOffset)
|
|
minOffset = t1->GetOffset();
|
|
if (t1->GetEndTime() > maxEnd)
|
|
maxEnd = t1->GetEndTime();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Otherwise, check for a stereo pair
|
|
t1 = t->GetLink();
|
|
if (t1)
|
|
{
|
|
if (t1->GetOffset() < minOffset)
|
|
minOffset = t1->GetOffset();
|
|
if (t1->GetEndTime() > maxEnd)
|
|
maxEnd = t1->GetEndTime();
|
|
}
|
|
}
|
|
|
|
// PRL: double click or click on track control.
|
|
// should this select all frequencies too? I think not.
|
|
mViewInfo->selectedRegion.setTimes(minOffset, maxEnd);
|
|
}
|
|
|
|
void TrackPanel::GetTracksUsableArea(int *width, int *height) const
|
|
{
|
|
GetSize(width, height);
|
|
*width -= GetLabelWidth();
|
|
// AS: MAGIC NUMBER: What does 2 represent?
|
|
*width -= 2 + kLeftInset;
|
|
*width = std::max(0, *width);
|
|
}
|
|
|
|
/// Gets the pointer to the AudacityProject that
|
|
/// goes with this track panel.
|
|
AudacityProject * TrackPanel::GetProject() const
|
|
{
|
|
//JKC casting away constness here.
|
|
//Do it in two stages in case 'this' is not a wxWindow.
|
|
//when the compiler will flag the error.
|
|
wxWindow const * const pConstWind = this;
|
|
wxWindow * pWind=(wxWindow*)pConstWind;
|
|
#ifdef EXPERIMENTAL_NOTEBOOK
|
|
pWind = pWind->GetParent(); //Page
|
|
wxASSERT( pWind );
|
|
pWind = pWind->GetParent(); //Notebook
|
|
wxASSERT( pWind );
|
|
#endif
|
|
pWind = pWind->GetParent(); //MainPanel
|
|
wxASSERT( pWind );
|
|
pWind = pWind->GetParent(); //Project
|
|
wxASSERT( pWind );
|
|
return (AudacityProject*)pWind;
|
|
}
|
|
|
|
/// AS: This gets called on our wx timer events.
|
|
void TrackPanel::OnTimer()
|
|
{
|
|
mTimeCount++;
|
|
// AS: If the user is dragging the mouse and there is a track that
|
|
// has captured the mouse, then scroll the screen, as necessary.
|
|
if ((mMouseCapture==IsSelecting) && mCapturedTrack) {
|
|
ScrollDuringDrag();
|
|
}
|
|
|
|
wxCommandEvent dummyEvent;
|
|
AudacityProject *p = GetProject();
|
|
|
|
{
|
|
wxCommandEvent e(EVT_TRACK_PANEL_TIMER);
|
|
p->ProcessEvent(e);
|
|
}
|
|
|
|
#ifdef EXPERIMENTAL_SCRUBBING_BASIC
|
|
// Call ContinueScrubbing() here rather than in SelectionHandleDrag()
|
|
// so that even without drag events, we can instruct the play head to
|
|
// keep approaching the mouse cursor, when its maximum speed is limited.
|
|
|
|
// Thus scrubbing relies mostly on periodic polling of mouse and keys,
|
|
// not event notifications. But there are a few event handlers that
|
|
// leave messages for this routine, in mScrubSeekPress and in mScrubHasFocus.
|
|
if (IsScrubbing())
|
|
{
|
|
wxMouseState state(::wxGetMouseState());
|
|
wxCoord position = state.GetX();
|
|
const bool seek = mScrubSeekPress || PollIsSeeking();
|
|
ScreenToClient(&position, NULL);
|
|
if (ContinueScrubbing(position, mScrubHasFocus, seek))
|
|
mScrubSeekPress = false;
|
|
// else, if seek requested, try again at a later time when we might
|
|
// enqueue a long enough stutter
|
|
|
|
#ifdef EXPERIMENTAL_SCRUBBING_SMOOTH_SCROLL
|
|
if (mSmoothScrollingScrub)
|
|
// Redraw with every timer tick, to keep the indicator centered.
|
|
Refresh(false);
|
|
else
|
|
#endif
|
|
{
|
|
if (mScrubSpeedDisplayCountdown > 0) {
|
|
--mScrubSpeedDisplayCountdown;
|
|
if (mScrubSpeedDisplayCountdown == kOneSecondCountdown ||
|
|
mScrubSpeedDisplayCountdown == 0)
|
|
// Show or hide the maximum speed.
|
|
Refresh(false);
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// Check whether we were playing or recording, but the stream has stopped.
|
|
if (p->GetAudioIOToken()>0 && !IsAudioActive())
|
|
{
|
|
//the stream may have been started up after this one finished (by some other project)
|
|
//in that case reset the buttons don't stop the stream
|
|
p->GetControlToolBar()->StopPlaying(!gAudioIO->IsStreamActive());
|
|
}
|
|
|
|
// Next, check to see if we were playing or recording
|
|
// audio, but now Audio I/O is completely finished.
|
|
if (p->GetAudioIOToken()>0 &&
|
|
!gAudioIO->IsAudioTokenActive(p->GetAudioIOToken()))
|
|
{
|
|
p->FixScrollbars();
|
|
p->SetAudioIOToken(0);
|
|
p->RedrawProject();
|
|
|
|
mRedrawAfterStop = false;
|
|
|
|
//ANSWER-ME: Was DisplaySelection added to solve a repaint problem?
|
|
DisplaySelection();
|
|
}
|
|
|
|
// AS: The "indicator" is the little graphical mark shown in the ruler
|
|
// that indicates where the current play/record position is. (This also
|
|
// draws the moving vertical line.)
|
|
|
|
if (!gAudioIO->IsPaused() && ( mIndicatorShowing || IsAudioActive())
|
|
|
|
#ifdef EXPERIMENTAL_SCRUBBING_SMOOTH_SCROLL
|
|
&& !mSmoothScrollingScrub
|
|
#endif
|
|
)
|
|
{
|
|
DrawIndicator();
|
|
}
|
|
|
|
if(IsAudioActive() && gAudioIO->GetNumCaptureChannels()) {
|
|
|
|
// Periodically update the display while recording
|
|
|
|
if (!mRedrawAfterStop) {
|
|
mRedrawAfterStop = true;
|
|
MakeParentRedrawScrollbars();
|
|
mListener->TP_ScrollUpDown( 99999999 );
|
|
Refresh( false );
|
|
}
|
|
else {
|
|
if ((mTimeCount % 5) == 0) {
|
|
// Must tell OnPaint() to recreate the backing bitmap
|
|
// since we've not done a full refresh.
|
|
mRefreshBacking = true;
|
|
Refresh( false );
|
|
}
|
|
}
|
|
}
|
|
if(mTimeCount > 1000)
|
|
mTimeCount = 0;
|
|
}
|
|
|
|
/// We check on each timer tick to see if we need to scroll.
|
|
/// Scrolling is handled by mListener, which is an interface
|
|
/// to the window TrackPanel is embedded in.
|
|
void TrackPanel::ScrollDuringDrag()
|
|
{
|
|
// DM: If we're "autoscrolling" (which means that we're scrolling
|
|
// because the user dragged from inside to outside the window,
|
|
// not because the user clicked in the scroll bar), then
|
|
// the selection code needs to be handled slightly differently.
|
|
// We set this flag ("mAutoScrolling") to tell the selecting
|
|
// code that we didn't get here as a result of a mouse event,
|
|
// and therefore it should ignore the mouseEvent parameter,
|
|
// and instead use the last known mouse position. Setting
|
|
// this flag also causes the Mac to redraw immediately rather
|
|
// than waiting for the next update event; this makes scrolling
|
|
// smoother on MacOS 9.
|
|
|
|
if (mMouseMostRecentX >= mCapturedRect.x + mCapturedRect.width) {
|
|
mAutoScrolling = true;
|
|
mListener->TP_ScrollRight();
|
|
}
|
|
else if (mMouseMostRecentX < mCapturedRect.x) {
|
|
mAutoScrolling = true;
|
|
mListener->TP_ScrollLeft();
|
|
}
|
|
|
|
if (mAutoScrolling) {
|
|
// AS: To keep the selection working properly as we scroll,
|
|
// we fake a mouse event (remember, this method is called
|
|
// from a timer tick).
|
|
|
|
// AS: For some reason, GCC won't let us pass this directly.
|
|
wxMouseEvent e(wxEVT_MOTION);
|
|
HandleSelect(e);
|
|
mAutoScrolling = false;
|
|
}
|
|
}
|
|
|
|
/// This updates the indicator (on a timer tick) that shows
|
|
/// where the current play or record position is. To do this,
|
|
/// we cheat a little. The indicator is drawn during the ruler
|
|
/// drawing process (that should probably change, but...), so
|
|
/// we create a memory DC and tell the ruler to draw itself there,
|
|
/// and then just blit that to the screen.
|
|
/// The indicator is a small triangle, red for record, green for play.
|
|
void TrackPanel::DrawIndicator()
|
|
{
|
|
wxClientDC dc( this );
|
|
DoDrawIndicator( dc );
|
|
}
|
|
|
|
void TrackPanel::DrawQuickPlayIndicator(wxDC & dc, double pos)
|
|
{
|
|
// Erase the old indicator.
|
|
if( mOldQPIndicatorPos != -1 ) {
|
|
dc.Blit( mOldQPIndicatorPos, 0, 1, mBacking->GetHeight(), &mBackingDC, mOldQPIndicatorPos, 0 );
|
|
mOldQPIndicatorPos = -1;
|
|
}
|
|
|
|
if (pos >= 0) {
|
|
|
|
(mRuler->mIsSnapped)? AColor::SnapGuidePen( &dc) : AColor::Light( &dc, false);
|
|
|
|
// Draw indicator in all visible tracks
|
|
VisibleTrackIterator iter( GetProject() );
|
|
for( Track *t = iter.First(); t; t = iter.Next() )
|
|
{
|
|
// Convert virtual coordinate to physical
|
|
int y = t->GetY() - mViewInfo->vpos;
|
|
|
|
// Draw the new indicator in its new location
|
|
AColor::Line(dc,
|
|
pos,
|
|
y + kTopInset + 1,
|
|
pos,
|
|
y + t->GetHeight() - 3 );
|
|
}
|
|
mOldQPIndicatorPos = pos;
|
|
}
|
|
}
|
|
|
|
/// Second level DrawIndicator()
|
|
void TrackPanel::DoDrawIndicator
|
|
(wxDC & dc, bool repairOld /* = false */, double indicator /* = -1 */)
|
|
{
|
|
bool onScreen;
|
|
double pos;
|
|
|
|
if (!repairOld)
|
|
{
|
|
// Erase the old indicator.
|
|
if (mLastIndicatorX != -1)
|
|
{
|
|
onScreen = between_inclusive(GetLeftOffset(),
|
|
mLastIndicatorX,
|
|
GetLeftOffset() + mViewInfo->GetScreenWidth());
|
|
if (onScreen)
|
|
{
|
|
// LL: Keep from trying to blit outsize of the source DC. This results in a crash on
|
|
// OSX due to allocating memory using negative sizes and can be caused by resizing
|
|
// the project window while recording or playing.
|
|
int w = dc.GetSize().GetWidth();
|
|
if (mLastIndicatorX >= w) {
|
|
mLastIndicatorX = w - 1;
|
|
}
|
|
|
|
dc.Blit(mLastIndicatorX, 0, 1, mBacking->GetHeight(), &mBackingDC, mLastIndicatorX, 0);
|
|
}
|
|
|
|
// Nothing's ever perfect...
|
|
//
|
|
// Redraw the cursor since we may have just wiped it out
|
|
if (mLastCursorX == mLastIndicatorX)
|
|
{
|
|
DoDrawCursor( dc );
|
|
}
|
|
}
|
|
|
|
pos = indicator;
|
|
if (pos < 0.0)
|
|
// The stream time can be < 0 if the audio is currently stopped
|
|
pos = gAudioIO->GetStreamTime();
|
|
|
|
AudacityProject *p = GetProject();
|
|
bool audioActive = IsAudioActive();
|
|
onScreen = between_inclusive( mViewInfo->h,
|
|
pos,
|
|
mViewInfo->h + mViewInfo->screen );
|
|
|
|
// This displays the audio time, too...
|
|
DisplaySelection();
|
|
|
|
// BG: Scroll screen if option is set
|
|
// msmeyer: But only if not playing looped or in one-second mode
|
|
if( mViewInfo->bUpdateTrackIndicator &&
|
|
p->mLastPlayMode != loopedPlay &&
|
|
p->mLastPlayMode != oneSecondPlay &&
|
|
audioActive &&
|
|
pos >= 0 &&
|
|
!onScreen &&
|
|
!gAudioIO->IsPaused() )
|
|
{
|
|
mListener->TP_ScrollWindow( pos );
|
|
}
|
|
|
|
// Always update scrollbars even if not scrolling the window. This is
|
|
// important when new audio is recorded, because this can change the
|
|
// length of the project and therefore the appearance of the scrollbar.
|
|
MakeParentRedrawScrollbars();
|
|
|
|
mIndicatorShowing = ( onScreen && audioActive );
|
|
|
|
// Calculate the horizontal position of the indicator
|
|
mLastIndicatorX = mViewInfo->TimeToPosition(pos, GetLeftOffset());
|
|
}
|
|
else
|
|
pos = mViewInfo->PositionToTime(mLastIndicatorX, GetLeftOffset());
|
|
|
|
// Set play/record color
|
|
bool rec = (gAudioIO->GetNumCaptureChannels() > 0);
|
|
AColor::IndicatorColor( &dc, !rec);
|
|
|
|
mRuler->DrawIndicator( pos, rec );
|
|
|
|
// Ensure that we don't draw through the TrackInfo or vertical ruler.
|
|
wxRect clip = GetRect();
|
|
int leftCutoff = clip.x + GetLabelWidth();
|
|
int rightInset = kLeftInset + 2; // See the call to SetInset
|
|
int rightCutoff = clip.x + clip.width - rightInset;
|
|
if (!between_inclusive(leftCutoff, mLastIndicatorX, rightCutoff))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Draw indicator in all visible tracks
|
|
VisibleTrackIterator iter( GetProject() );
|
|
for( Track *t = iter.First(); t; t = iter.Next() )
|
|
{
|
|
// Don't draw the indicator in label tracks
|
|
if( t->GetKind() == Track::Label )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Convert virtual coordinate to physical
|
|
int y = t->GetY() - mViewInfo->vpos;
|
|
|
|
// Draw the new indicator in its new location
|
|
AColor::Line(dc,
|
|
mLastIndicatorX,
|
|
y + kTopInset + 1,
|
|
mLastIndicatorX,
|
|
y + t->GetHeight() - 3 );
|
|
}
|
|
}
|
|
|
|
/// This method draws the cursor things, both in the
|
|
/// ruler as seen at the top of the screen, but also in each of the
|
|
/// selected tracks.
|
|
/// These are the 'vertical lines' through waves, notes, and ruler.
|
|
void TrackPanel::DrawCursor()
|
|
{
|
|
wxClientDC dc( this );
|
|
DoDrawCursor( dc );
|
|
}
|
|
|
|
/// Second level DrawCursor()
|
|
void TrackPanel::DoDrawCursor(wxDC & dc)
|
|
{
|
|
bool onScreen;
|
|
|
|
if( mLastCursorX != -1 )
|
|
{
|
|
onScreen = between_inclusive(GetLeftOffset(),
|
|
mLastCursorX,
|
|
GetLeftOffset() + mViewInfo->GetScreenWidth());
|
|
if( onScreen )
|
|
dc.Blit(mLastCursorX, 0, 1, mBacking->GetHeight(), &mBackingDC, mLastCursorX, 0);
|
|
}
|
|
|
|
const double time = mViewInfo->selectedRegion.t0();
|
|
mLastCursorX = mViewInfo->TimeToPosition(time, GetLeftOffset());
|
|
|
|
onScreen = between_inclusive( mViewInfo->h,
|
|
time,
|
|
mViewInfo->h + mViewInfo->screen );
|
|
|
|
if( !onScreen )
|
|
return;
|
|
|
|
AColor::CursorColor( &dc );
|
|
|
|
// Draw cursor in all selected tracks
|
|
VisibleTrackIterator iter( GetProject() );
|
|
for( Track *t = iter.First(); t; t = iter.Next() )
|
|
{
|
|
if( t->GetSelected() || mAx->IsFocused( t ) )
|
|
{
|
|
int y = t->GetY() - mViewInfo->vpos + 1;
|
|
wxCoord top = y + kTopInset;
|
|
wxCoord bottom = y + t->GetHeight() - kTopInset;
|
|
|
|
// MB: warp() is not needed here as far as I know, in fact it creates a bug. Removing it fixes that.
|
|
AColor::Line(dc, mLastCursorX, top, mLastCursorX, bottom); // <-- The whole point of this routine.
|
|
|
|
#ifdef EXPERIMENTAL_OUTPUT_DISPLAY
|
|
if(MONO_WAVE_PAN(t)){
|
|
y = t->GetY(true) - mViewInfo->vpos + 1;
|
|
top = y + kTopInset;
|
|
bottom = y + t->GetHeight(true) - kTopInset;
|
|
AColor::Line( dc, mLastCursorX, top, mLastCursorX, bottom );
|
|
}
|
|
#endif
|
|
|
|
}
|
|
}
|
|
|
|
// AS: Ah, no, this is where we draw the blinky thing in the ruler.
|
|
mRuler->DrawCursor( time );
|
|
|
|
DisplaySelection();
|
|
}
|
|
|
|
/// OnSize() is called when the panel is resized
|
|
void TrackPanel::OnSize(wxSizeEvent & /* event */)
|
|
{
|
|
int width, height;
|
|
GetSize( &width, &height );
|
|
|
|
// wxMac doesn't like zero dimensions, so protect against it
|
|
width = width == 0 ? 1 : width;
|
|
height = height == 0 ? 1 : height;
|
|
|
|
// (Re)allocate the backing bitmap
|
|
if( mBacking )
|
|
{
|
|
mBackingDC.SelectObject( wxNullBitmap );
|
|
delete mBacking;
|
|
}
|
|
|
|
mBacking = new wxBitmap( width, height );
|
|
mBackingDC.SelectObject( *mBacking );
|
|
|
|
// Refresh the entire area. Really only need to refresh when
|
|
// expanding...is it worth the trouble?
|
|
Refresh( false );
|
|
}
|
|
|
|
/// OnErase( ) is called during the normal course of
|
|
/// completing an erase operation.
|
|
void TrackPanel::OnErase(wxEraseEvent & /* event */)
|
|
{
|
|
// Ignore it for now. This reduces flashing when dragging windows
|
|
// over track area while playing or recording.
|
|
//
|
|
// However, if artifacts or the like are discovered later, then
|
|
// we could blit the backing bitmap here.
|
|
}
|
|
|
|
/// AS: OnPaint( ) is called during the normal course of
|
|
/// completing a repaint operation.
|
|
void TrackPanel::OnPaint(wxPaintEvent & /* event */)
|
|
{
|
|
#if DEBUG_DRAW_TIMING
|
|
wxStopWatch sw;
|
|
#endif
|
|
|
|
// Construct the paint DC on the heap so that it may be deleted
|
|
// early
|
|
wxDC *dc = new wxPaintDC(this);
|
|
|
|
// Retrieve the damage rectangle
|
|
wxRect box = GetUpdateRegion().GetBox();
|
|
|
|
double indicator = -1;
|
|
|
|
// Recreate the backing bitmap if we have a full refresh
|
|
// (See TrackPanel::Refresh())
|
|
if (mRefreshBacking || (box == GetRect()))
|
|
{
|
|
// Reset (should a mutex be used???)
|
|
mRefreshBacking = false;
|
|
|
|
#ifdef EXPERIMENTAL_SCRUBBING_SMOOTH_SCROLL
|
|
if (mSmoothScrollingScrub && IsAudioActive()) {
|
|
// Pan the view, so that we center the play indicator.
|
|
// By the time DoDrawIndicator() is reached, gAudioIO->GetStreamTime()
|
|
// may be a little different.
|
|
// Cause DoDrawIndicator to show the value as it was at this time when we begin
|
|
// the drawing of the tracks. This prevents flashing of the indicator
|
|
// at higher magnifications, and keeps the green line still in the middle.
|
|
indicator = gAudioIO->GetStreamTime();
|
|
mViewInfo->h = indicator - mViewInfo->screen / 2.0;
|
|
if (!mScrollBeyondZero)
|
|
// Can't scroll too far left
|
|
mViewInfo->h = std::max(0.0, mViewInfo->h);
|
|
}
|
|
#endif
|
|
|
|
// Redraw the backing bitmap
|
|
DrawTracks(&mBackingDC);
|
|
|
|
// Copy it to the display
|
|
dc->Blit(0, 0, mBacking->GetWidth(), mBacking->GetHeight(), &mBackingDC, 0, 0);
|
|
}
|
|
else
|
|
{
|
|
// Copy full, possibly clipped, damage rectange
|
|
dc->Blit(box.x, box.y, box.width, box.height, &mBackingDC, box.x, box.y);
|
|
}
|
|
|
|
// Done with the clipped DC
|
|
delete dc;
|
|
|
|
// Drawing now goes directly to the client area
|
|
wxClientDC cdc(this);
|
|
|
|
// Update the indicator in case it was damaged if this project is playing
|
|
|
|
if (!gAudioIO->IsPaused() && (mIndicatorShowing || IsAudioActive()))
|
|
{
|
|
// If not smooth scrolling, then
|
|
// we just want to repair, not update the old, so set the second param to true.
|
|
// This is important because this onPaint could be for just some of the tracks.
|
|
DoDrawIndicator(cdc, (indicator < 0), indicator);
|
|
}
|
|
|
|
// Draw the cursor
|
|
if (mViewInfo->selectedRegion.isPoint())
|
|
DoDrawCursor(cdc);
|
|
|
|
#if DEBUG_DRAW_TIMING
|
|
sw.Pause();
|
|
wxLogDebug(wxT("Total: %ld milliseconds"), sw.Time());
|
|
wxPrintf(wxT("Total: %ld milliseconds\n"), sw.Time());
|
|
#endif
|
|
}
|
|
|
|
/// Makes our Parent (well, whoever is listening to us) push their state.
|
|
/// this causes application state to be preserved on a stack for undo ops.
|
|
void TrackPanel::MakeParentPushState(wxString desc, wxString shortDesc,
|
|
int flags)
|
|
{
|
|
mListener->TP_PushState(desc, shortDesc, flags);
|
|
}
|
|
|
|
void TrackPanel::MakeParentModifyState(bool bWantsAutoSave)
|
|
{
|
|
mListener->TP_ModifyState(bWantsAutoSave);
|
|
}
|
|
|
|
void TrackPanel::MakeParentRedrawScrollbars()
|
|
{
|
|
mListener->TP_RedrawScrollbars();
|
|
}
|
|
|
|
void TrackPanel::MakeParentResize()
|
|
{
|
|
mListener->TP_HandleResize();
|
|
}
|
|
|
|
void TrackPanel::HandleEscapeKey(bool down)
|
|
{
|
|
if (!down)
|
|
return;
|
|
|
|
switch (mMouseCapture)
|
|
{
|
|
case IsSelecting:
|
|
{
|
|
TrackListIterator iter(mTracks);
|
|
std::vector<bool>::const_iterator
|
|
it = mInitialTrackSelection->begin(),
|
|
end = mInitialTrackSelection->end();
|
|
for (Track *t = iter.First(); t; t = iter.Next()) {
|
|
wxASSERT(it != end);
|
|
t->SetSelected(*it++);
|
|
}
|
|
mViewInfo->selectedRegion = mInitialSelection;
|
|
}
|
|
break;
|
|
case IsZooming:
|
|
case IsVZooming:
|
|
//case IsAdjustingSample:
|
|
break;
|
|
case IsResizing:
|
|
mCapturedTrack->SetHeight(mInitialActualHeight);
|
|
mCapturedTrack->SetMinimized(mInitialMinimized);
|
|
break;
|
|
case IsResizingBetweenLinkedTracks:
|
|
{
|
|
Track *const next = mTracks->GetNext(mCapturedTrack);
|
|
mCapturedTrack->SetHeight(mInitialUpperActualHeight);
|
|
mCapturedTrack->SetMinimized(mInitialMinimized);
|
|
next->SetHeight(mInitialActualHeight);
|
|
next->SetMinimized(mInitialMinimized);
|
|
}
|
|
break;
|
|
case IsResizingBelowLinkedTracks:
|
|
{
|
|
Track *const prev = mTracks->GetPrev(mCapturedTrack);
|
|
mCapturedTrack->SetHeight(mInitialActualHeight);
|
|
mCapturedTrack->SetMinimized(mInitialMinimized);
|
|
prev->SetHeight(mInitialUpperActualHeight);
|
|
prev->SetMinimized(mInitialMinimized);
|
|
}
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
|
|
// Common part in all cases that do anything
|
|
SetCapturedTrack(NULL, IsUncaptured);
|
|
if (HasCapture())
|
|
ReleaseMouse();
|
|
wxMouseEvent dummy;
|
|
HandleCursor(dummy);
|
|
Refresh(false);
|
|
}
|
|
|
|
void TrackPanel::HandleAltKey(bool down)
|
|
{
|
|
mLastMouseEvent.m_altDown = down;
|
|
HandleCursorForLastMouseEvent();
|
|
}
|
|
|
|
void TrackPanel::HandleShiftKey(bool down)
|
|
{
|
|
mLastMouseEvent.m_shiftDown = down;
|
|
HandleCursorForLastMouseEvent();
|
|
}
|
|
|
|
void TrackPanel::HandleControlKey(bool down)
|
|
{
|
|
mLastMouseEvent.m_controlDown = down;
|
|
HandleCursorForLastMouseEvent();
|
|
}
|
|
|
|
void TrackPanel::HandlePageUpKey()
|
|
{
|
|
mListener->TP_ScrollWindow(mViewInfo->h + mViewInfo->screen);
|
|
}
|
|
|
|
void TrackPanel::HandlePageDownKey()
|
|
{
|
|
mListener->TP_ScrollWindow(mViewInfo->h - mViewInfo->screen);
|
|
}
|
|
|
|
void TrackPanel::HandleCursorForLastMouseEvent()
|
|
{
|
|
HandleCursor(mLastMouseEvent);
|
|
}
|
|
|
|
MixerBoard* TrackPanel::GetMixerBoard()
|
|
{
|
|
AudacityProject *p = GetProject();
|
|
wxASSERT(p);
|
|
return p->GetMixerBoard();
|
|
}
|
|
|
|
/// Used to determine whether it is safe or not to perform certain
|
|
/// edits at the moment.
|
|
/// @return true if audio is being recorded or is playing.
|
|
bool TrackPanel::IsUnsafe()
|
|
{
|
|
return IsAudioActive();
|
|
}
|
|
|
|
bool TrackPanel::IsAudioActive()
|
|
{
|
|
AudacityProject *p = GetProject();
|
|
return p->IsAudioActive();
|
|
}
|
|
|
|
|
|
/// Uses a previously noted 'activity' to determine what
|
|
/// cursor to use.
|
|
/// @var mMouseCapture holds the current activity.
|
|
bool TrackPanel::SetCursorByActivity( )
|
|
{
|
|
bool unsafe = IsUnsafe();
|
|
|
|
switch( mMouseCapture )
|
|
{
|
|
case IsSelecting:
|
|
SetCursor(*mSelectCursor);
|
|
return true;
|
|
case IsSliding:
|
|
SetCursor( unsafe ? *mDisabledCursor : *mSlideCursor);
|
|
return true;
|
|
case IsEnveloping:
|
|
SetCursor( unsafe ? *mDisabledCursor : *mEnvelopeCursor);
|
|
return true;
|
|
case IsRearranging:
|
|
SetCursor( unsafe ? *mDisabledCursor : *mRearrangeCursor);
|
|
return true;
|
|
case IsAdjustingLabel:
|
|
return true;
|
|
#ifdef USE_MIDI
|
|
case IsStretching:
|
|
SetCursor( unsafe ? *mDisabledCursor : *mStretchCursor);
|
|
return true;
|
|
#endif
|
|
default:
|
|
break;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool TrackPanel::SetCursorForCutline(WaveTrack * track, wxRect &rect, wxMouseEvent &event)
|
|
{
|
|
if (IsOverCutline(track, rect, event)) {
|
|
bool unsafe = IsUnsafe();
|
|
SetCursor(unsafe ? *mDisabledCursor : *mArrowCursor);
|
|
return true;
|
|
// No tip string?
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// When in the "label" (TrackInfo or vertical ruler), we can either vertical zoom or re-order tracks.
|
|
/// Dont't change cursor/tip to zoom if display is not waveform (either linear of dB) or Spectrum
|
|
void TrackPanel::SetCursorAndTipWhenInLabel( Track * t,
|
|
wxMouseEvent &event, wxString &tip )
|
|
{
|
|
if (event.m_x >= GetVRulerOffset() && (t->GetKind() == Track::Wave) )
|
|
{
|
|
tip = _("Click to vertically zoom in. Shift-click to zoom out. Drag to specify a zoom region.");
|
|
SetCursor(event.ShiftDown()? *mZoomOutCursor : *mZoomInCursor);
|
|
}
|
|
#ifdef USE_MIDI
|
|
else if (event.m_x >= GetVRulerOffset() && t->GetKind() == Track::Note) {
|
|
tip = _("Click to verticaly zoom in, Shift-click to zoom out, Drag to create a particular zoom region.");
|
|
SetCursor(event.ShiftDown() ? *mZoomOutCursor : *mZoomInCursor);
|
|
}
|
|
#endif
|
|
else {
|
|
// Set a status message if over TrackInfo.
|
|
tip = _("Drag the track vertically to change the order of the tracks.");
|
|
SetCursor(*mArrowCursor);
|
|
}
|
|
}
|
|
|
|
/// When in the resize area we can adjust size or relative size.
|
|
void TrackPanel::SetCursorAndTipWhenInVResizeArea( bool bLinked, wxString &tip )
|
|
{
|
|
// Check to see whether it is the first channel of a stereo track
|
|
if (bLinked) {
|
|
// If we are in the label we got here 'by mistake' and we're
|
|
// not actually in the resize area at all. (The resize area
|
|
// is shorter when it is between stereo tracks).
|
|
|
|
tip = _("Click and drag to adjust relative size of stereo tracks.");
|
|
SetCursor(*mResizeCursor);
|
|
} else {
|
|
tip = _("Click and drag to resize the track.");
|
|
SetCursor(*mResizeCursor);
|
|
}
|
|
}
|
|
|
|
/// When in a label track, find out if we've hit anything that
|
|
/// would cause a cursor change.
|
|
void TrackPanel::SetCursorAndTipWhenInLabelTrack( LabelTrack * pLT,
|
|
wxMouseEvent & event, wxString &tip )
|
|
{
|
|
int edge=pLT->OverGlyph(event.m_x, event.m_y);
|
|
if(edge !=0)
|
|
{
|
|
SetCursor(*mArrowCursor);
|
|
}
|
|
|
|
//KLUDGE: We refresh the whole Label track when the icon hovered over
|
|
//changes colouration. As well as being inefficient we are also
|
|
//doing stuff that should be delegated to the label track itself.
|
|
edge += pLT->mbHitCenter ? 4:0;
|
|
if( edge != pLT->mOldEdge )
|
|
{
|
|
pLT->mOldEdge = edge;
|
|
RefreshTrack( pLT );
|
|
}
|
|
// IF edge!=0 THEN we've set the cursor and we're done.
|
|
// signal this by setting the tip.
|
|
if( edge != 0 )
|
|
{
|
|
tip =
|
|
(pLT->mbHitCenter ) ?
|
|
_("Drag one or more label boundaries.") :
|
|
_("Drag label boundary.");
|
|
}
|
|
}
|
|
|
|
namespace {
|
|
|
|
// This returns true if we're a spectral editing track.
|
|
inline bool isSpectralSelectionTrack(const Track *pTrack) {
|
|
if (pTrack &&
|
|
pTrack->GetKind() == Track::Wave) {
|
|
const WaveTrack *const wt = static_cast<const WaveTrack*>(pTrack);
|
|
const SpectrogramSettings &settings = wt->GetSpectrogramSettings();
|
|
const int display = wt->GetDisplay();
|
|
return (display == WaveTrack::Spectrum) && settings.SpectralSelectionEnabled();
|
|
}
|
|
else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
// If we're in OnDemand mode, we may change the tip.
|
|
void TrackPanel::MaySetOnDemandTip( Track * t, wxString &tip )
|
|
{
|
|
wxASSERT( t );
|
|
//For OD regions, we need to override and display the percent complete for this task.
|
|
//first, make sure it's a wavetrack.
|
|
if(t->GetKind() != Track::Wave)
|
|
return;
|
|
//see if the wavetrack exists in the ODManager (if the ODManager exists)
|
|
if(!ODManager::IsInstanceCreated())
|
|
return;
|
|
//ask the wavetrack for the corresponding tip - it may not change tip, but that's fine.
|
|
ODManager::Instance()->FillTipForWaveTrack(static_cast<WaveTrack*>(t), tip);
|
|
return;
|
|
}
|
|
|
|
#ifdef EXPERIMENTAL_SPECTRAL_EDITING
|
|
void TrackPanel::HandleCenterFrequencyCursor
|
|
(bool shiftDown, wxString &tip, const wxCursor ** ppCursor)
|
|
{
|
|
#ifndef SPECTRAL_EDITING_ESC_KEY
|
|
tip =
|
|
shiftDown ?
|
|
_("Click and drag to move center selection frequency.") :
|
|
_("Click and drag to move center selection frequency to a spectral peak.");
|
|
|
|
#else
|
|
shiftDown;
|
|
|
|
tip =
|
|
_("Click and drag to move center selection frequency.");
|
|
|
|
#endif
|
|
|
|
*ppCursor = mEnvelopeCursor;
|
|
}
|
|
|
|
void TrackPanel::HandleCenterFrequencyClick
|
|
(bool shiftDown, Track *pTrack, double value)
|
|
{
|
|
if (shiftDown) {
|
|
// Disable time selection
|
|
mSelStartValid = false;
|
|
mFreqSelTrack = static_cast<WaveTrack*>(pTrack);
|
|
mFreqSelPin = value;
|
|
mFreqSelMode = FREQ_SEL_DRAG_CENTER;
|
|
}
|
|
else {
|
|
#ifndef SPECTRAL_EDITING_ESC_KEY
|
|
// Start center snapping
|
|
WaveTrack *wt = static_cast<WaveTrack*>(pTrack);
|
|
// Turn center snapping on (the only way to do this)
|
|
mFreqSelMode = FREQ_SEL_SNAPPING_CENTER;
|
|
// Disable time selection
|
|
mSelStartValid = false;
|
|
StartSnappingFreqSelection(wt);
|
|
#endif
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// The select tool can have different cursors and prompts depending on what
|
|
// we hover over, most notably when hovering over the selction boundaries.
|
|
// Determine and set the cursor and tip accordingly.
|
|
void TrackPanel::SetCursorAndTipWhenSelectTool( Track * t,
|
|
wxMouseEvent & event, wxRect &r, bool bMultiToolMode,
|
|
wxString &tip, const wxCursor ** ppCursor )
|
|
{
|
|
// Do not set the default cursor here and re-set later, that causes
|
|
// flashing.
|
|
*ppCursor = mSelectCursor;
|
|
|
|
//In Multi-tool mode, give multitool prompt if no-special-hit.
|
|
if( bMultiToolMode ) {
|
|
// Look up the current key binding for Preferences.
|
|
// (Don't assume it's the default!)
|
|
wxString keyStr
|
|
(GetProject()->GetCommandManager()->GetKeyFromName(wxT("Preferences")));
|
|
if (keyStr.IsEmpty())
|
|
// No keyboard preference defined for opening Preferences dialog
|
|
/* i18n-hint: These are the names of a menu and a command in that menu */
|
|
keyStr = _("Edit, Preferences...");
|
|
else
|
|
keyStr = KeyStringDisplay(keyStr);
|
|
/* i18n-hint: %s is usually replaced by "Ctrl+P" for Windows/Linux, "Command+," for Mac */
|
|
tip = wxString::Format(
|
|
_("Multi-Tool Mode: %s for Mouse and Keyboard Preferences."),
|
|
keyStr.c_str());
|
|
// Later in this function we may point to some other string instead.
|
|
}
|
|
|
|
// Not over a track? Get out of here.
|
|
if(!t)
|
|
return;
|
|
|
|
//Make sure we are within the selected track
|
|
// Adjusting the selection edges can be turned off in
|
|
// the preferences...
|
|
if ( !t->GetSelected() || !mAdjustSelectionEdges)
|
|
{
|
|
MaySetOnDemandTip( t, tip );
|
|
return;
|
|
}
|
|
|
|
{
|
|
wxInt64 leftSel = mViewInfo->TimeToPosition(mViewInfo->selectedRegion.t0(), r.x);
|
|
wxInt64 rightSel = mViewInfo->TimeToPosition(mViewInfo->selectedRegion.t1(), r.x);
|
|
// Something is wrong if right edge comes before left edge
|
|
wxASSERT(!(rightSel < leftSel));
|
|
}
|
|
|
|
const bool bShiftDown = event.ShiftDown();
|
|
|
|
#ifdef EXPERIMENTAL_SPECTRAL_EDITING
|
|
if ( (mFreqSelMode == FREQ_SEL_SNAPPING_CENTER) &&
|
|
isSpectralSelectionTrack(t)) {
|
|
// Not shift-down, but center frequency snapping toggle is on
|
|
tip = _("Click and drag to set frequency bandwidth.");
|
|
*ppCursor = mEnvelopeCursor;
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
// If not shift-down and not snapping center, then
|
|
// choose boundaries only in snapping tolerance,
|
|
// and may choose center.
|
|
// But don't change the cursor when scrubbing.
|
|
SelectionBoundary boundary =
|
|
#ifdef EXPERIMENTAL_SCRUBBING_BASIC
|
|
IsScrubbing()
|
|
? SBNone
|
|
:
|
|
#endif
|
|
ChooseBoundary(event, t, r, !bShiftDown, !bShiftDown);
|
|
|
|
#ifdef USE_MIDI
|
|
// The MIDI HitTest will only succeed if we are on a midi track, so
|
|
// typically we will fall through.
|
|
switch( boundary) {
|
|
case SBNone:
|
|
case SBLeft:
|
|
case SBRight:
|
|
if ( HitTestStretch(t, r, event)) {
|
|
tip = _("Click and drag to stretch within selected region.");
|
|
*ppCursor = mStretchCursor;
|
|
return;
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
#endif
|
|
|
|
switch (boundary) {
|
|
case SBNone:
|
|
if( bShiftDown ){
|
|
// wxASSERT( false );
|
|
// Same message is used for moving left right top or bottom edge.
|
|
tip = _("Click to move selection boundary to cursor.");
|
|
// No cursor change.
|
|
return;
|
|
}
|
|
break;
|
|
case SBLeft:
|
|
tip = _("Click and drag to move left selection boundary.");
|
|
*ppCursor = mAdjustLeftSelectionCursor;
|
|
return;
|
|
case SBRight:
|
|
tip = _("Click and drag to move right selection boundary.");
|
|
*ppCursor = mAdjustRightSelectionCursor;
|
|
return;
|
|
#ifdef EXPERIMENTAL_SPECTRAL_EDITING
|
|
case SBBottom:
|
|
tip = _("Click and drag to move bottom selection frequency.");
|
|
*ppCursor = mBottomFrequencyCursor;
|
|
return;
|
|
case SBTop:
|
|
tip = _("Click and drag to move top selection frequency.");
|
|
*ppCursor = mTopFrequencyCursor;
|
|
return;
|
|
case SBCenter:
|
|
HandleCenterFrequencyCursor(bShiftDown, tip, ppCursor);
|
|
return;
|
|
case SBWidth:
|
|
tip = _("Click and drag to adjust frequency bandwidth.");
|
|
*ppCursor = mBandWidthCursor;
|
|
return;
|
|
#endif
|
|
default:
|
|
wxASSERT(false);
|
|
} // switch
|
|
// Falls through the switch if there was no boundary found.
|
|
|
|
MaySetOnDemandTip( t, tip );
|
|
}
|
|
|
|
/// In this method we know what tool we are using,
|
|
/// so set the cursor accordingly.
|
|
void TrackPanel::SetCursorAndTipByTool( int tool,
|
|
wxMouseEvent & event, wxString& )
|
|
{
|
|
bool unsafe = IsUnsafe();
|
|
|
|
// Change the cursor based on the active tool.
|
|
switch (tool) {
|
|
case selectTool:
|
|
wxFAIL;// should have already been handled
|
|
break;
|
|
case envelopeTool:
|
|
SetCursor(unsafe ? *mDisabledCursor : *mEnvelopeCursor);
|
|
break;
|
|
case slideTool:
|
|
SetCursor(unsafe ? *mDisabledCursor : *mSlideCursor);
|
|
break;
|
|
case zoomTool:
|
|
SetCursor(event.ShiftDown()? *mZoomOutCursor : *mZoomInCursor);
|
|
break;
|
|
case drawTool:
|
|
if (unsafe)
|
|
SetCursor(*mDisabledCursor);
|
|
else
|
|
SetCursor(event.AltDown()? *mSmoothCursor : *mPencilCursor);
|
|
break;
|
|
}
|
|
// doesn't actually change the tip itself, but it could (should?) do at some
|
|
// future date.
|
|
}
|
|
|
|
/// TrackPanel::HandleCursor( ) sets the cursor drawn at the mouse location.
|
|
/// As this procedure checks which region the mouse is over, it is
|
|
/// appropriate to establish the message in the status bar.
|
|
void TrackPanel::HandleCursor(wxMouseEvent & event)
|
|
{
|
|
mLastMouseEvent = event;
|
|
|
|
// (1), If possible, set the cursor based on the current activity
|
|
// ( leave the StatusBar alone ).
|
|
if( SetCursorByActivity() )
|
|
return;
|
|
|
|
// (2) If we are not over a track at all, set the cursor to Arrow and
|
|
// clear the StatusBar,
|
|
wxRect labelRect, trackRect;
|
|
Track *const label = FindTrack(event.m_x, event.m_y, true, true, &labelRect);
|
|
Track *const nonlabel = FindTrack(event.m_x, event.m_y, false, false, &trackRect);
|
|
Track *const track = label ? label : nonlabel;
|
|
|
|
if (!track) {
|
|
SetCursor(*mArrowCursor);
|
|
mListener->TP_DisplayStatusMessage(wxT(""));
|
|
return;
|
|
}
|
|
|
|
// (3) The easy cases are done.
|
|
// Now we've got to hit-test against a number of different possibilities.
|
|
// We could be over the label or a vertical ruler etc...
|
|
|
|
// Strategy here is to set the tip when we have determined and
|
|
// set the correct cursor. We stop doing tests for what we've
|
|
// hit once the tip is not NULL.
|
|
|
|
wxString tip;
|
|
|
|
// Are we within the vertical resize area?
|
|
if (nonlabel
|
|
? within(event.m_y, trackRect.y + trackRect.height, TRACK_RESIZE_REGION)
|
|
: within(event.m_y, labelRect.y + labelRect.height, TRACK_RESIZE_REGION))
|
|
{
|
|
SetCursorAndTipWhenInVResizeArea(nonlabel && track->GetLinked(), tip);
|
|
// tip may still be NULL at this point, in which case we go on looking.
|
|
}
|
|
|
|
if ((tip == wxString()) && label) {
|
|
SetCursorAndTipWhenInLabel( label, event, tip );
|
|
}
|
|
|
|
// Otherwise, we must be over a track of some kind
|
|
// Is it a label track?
|
|
if ((tip == wxString()) && (track->GetKind() == Track::Label))
|
|
{
|
|
// We are over a label track
|
|
SetCursorAndTipWhenInLabelTrack( static_cast<LabelTrack*>(track), event, tip );
|
|
// ..and if we haven't yet determined the cursor,
|
|
// we go on to do all the standard track hit tests.
|
|
}
|
|
|
|
if ((tip == wxString()) &&
|
|
nonlabel &&
|
|
nonlabel->GetKind() == Track::Wave &&
|
|
SetCursorForCutline(static_cast<WaveTrack*>(nonlabel), trackRect, event))
|
|
return;
|
|
|
|
if( tip == wxString() )
|
|
{
|
|
ToolsToolBar * ttb = mListener->TP_GetToolsToolBar();
|
|
if( ttb == NULL )
|
|
return;
|
|
// JKC: DetermineToolToUse is called whenever the mouse
|
|
// moves. I had some worries about calling it when in
|
|
// multimode as it then has to hit-test all 'objects' in
|
|
// the track panel, but performance seems fine in
|
|
// practice (on a P500).
|
|
int tool = DetermineToolToUse( ttb, event );
|
|
|
|
tip = ttb->GetMessageForTool(tool);
|
|
|
|
// We don't include the select tool in
|
|
// SetCursorAndTipByTool() because it's more complex than
|
|
// the other tool cases.
|
|
if( tool != selectTool )
|
|
{
|
|
SetCursorAndTipByTool( tool, event, tip);
|
|
}
|
|
else
|
|
{
|
|
bool bMultiToolMode = ttb->IsDown(multiTool);
|
|
const wxCursor *pSelection = 0;
|
|
SetCursorAndTipWhenSelectTool
|
|
( track, event, trackRect, bMultiToolMode, tip, &pSelection );
|
|
if (pSelection)
|
|
// Set cursor once only here, to avoid flashing during drags
|
|
SetCursor(*pSelection);
|
|
}
|
|
}
|
|
|
|
if (tip != wxString())
|
|
mListener->TP_DisplayStatusMessage(tip);
|
|
}
|
|
|
|
|
|
/// This method handles various ways of starting and extending
|
|
/// selections. These are the selections you make by clicking and
|
|
/// dragging over a waveform.
|
|
void TrackPanel::HandleSelect(wxMouseEvent & event)
|
|
{
|
|
wxRect r;
|
|
Track *t = FindTrack(event.m_x, event.m_y, false, false, &r);
|
|
|
|
// AS: Ok, did the user just click the mouse, release the mouse,
|
|
// or drag?
|
|
if (event.LeftDown() ||
|
|
(event.LeftDClick() && event.CmdDown())) {
|
|
// AS: Now, did they click in a track somewhere? If so, we want
|
|
// to extend the current selection or start a new selection,
|
|
// depending on the shift key. If not, cancel all selections.
|
|
if (t)
|
|
SelectionHandleClick(event, t, r);
|
|
else {
|
|
SelectNone();
|
|
Refresh(false);
|
|
}
|
|
|
|
} else if (event.LeftUp() || event.RightUp()) {
|
|
if (mSnapManager) {
|
|
delete mSnapManager;
|
|
mSnapManager = NULL;
|
|
}
|
|
mSnapLeft = -1;
|
|
mSnapRight = -1;
|
|
|
|
SetCapturedTrack( NULL );
|
|
//Send the new selection state to the undo/redo stack:
|
|
MakeParentModifyState(false);
|
|
|
|
#ifdef EXPERIMENTAL_SPECTRAL_EDITING
|
|
// This stops center snapping with mouse movement
|
|
mFreqSelMode = FREQ_SEL_INVALID;
|
|
#endif
|
|
|
|
} else if (event.LeftDClick() && !event.ShiftDown()
|
|
#ifdef EXPERIMENTAL_SCRUBBING_SMOOTH_SCROLL
|
|
&& !event.CmdDown()
|
|
#endif
|
|
) {
|
|
if (!mCapturedTrack) {
|
|
wxRect r;
|
|
mCapturedTrack =
|
|
FindTrack(event.m_x, event.m_y, false, false, &r);
|
|
if (!mCapturedTrack)
|
|
return;
|
|
}
|
|
|
|
// Deselect all other tracks and select this one.
|
|
SelectNone();
|
|
|
|
mTracks->Select(mCapturedTrack);
|
|
|
|
// Default behavior: select whole track
|
|
SelectTrackLength(mCapturedTrack);
|
|
|
|
// Special case: if we're over a clip in a WaveTrack,
|
|
// select just that clip
|
|
if (mCapturedTrack->GetKind() == Track::Wave) {
|
|
WaveTrack *w = (WaveTrack *)mCapturedTrack;
|
|
WaveClip *selectedClip = w->GetClipAtX(event.m_x);
|
|
if (selectedClip) {
|
|
mViewInfo->selectedRegion.setTimes(
|
|
selectedClip->GetOffset(), selectedClip->GetEndTime());
|
|
}
|
|
//Also, capture this track for dragging until we up-click.
|
|
mCapturedClipArray.push_back(TrackClip(w, selectedClip));
|
|
|
|
mMouseCapture = IsSliding;
|
|
|
|
Refresh(false);
|
|
StartSlide(event);
|
|
goto done;
|
|
}
|
|
|
|
Refresh(false);
|
|
SetCapturedTrack( NULL );
|
|
MakeParentModifyState(false);
|
|
}
|
|
#ifdef EXPERIMENTAL_SPECTRAL_EDITING
|
|
#ifdef SPECTRAL_EDITING_ESC_KEY
|
|
else if (!event.IsButton() &&
|
|
mFreqSelMode == FREQ_SEL_SNAPPING_CENTER &&
|
|
!mViewInfo->selectedRegion.isPoint())
|
|
MoveSnappingFreqSelection(event.m_y, r.y, r.height, t);
|
|
#endif
|
|
#endif
|
|
done:
|
|
SelectionHandleDrag(event, t);
|
|
}
|
|
|
|
|
|
// Made obsolete by scrubbing:
|
|
#ifndef EXPERIMENTAL_SCRUBBING_BASIC
|
|
void TrackPanel::StartOrJumpPlayback(wxMouseEvent &event)
|
|
{
|
|
AudacityProject *p = GetActiveProject();
|
|
if (p) {
|
|
double clicktime = mViewInfo->PositionToTime(event.m_x, GetLeftOffset());
|
|
const double t1 = mViewInfo->selectedRegion.t1();
|
|
// Play to end of selection, or if that is not right of the pick, end of track
|
|
double endtime = clicktime < t1 ? t1 : mViewInfo->total;
|
|
|
|
//Behavior should differ depending upon whether we are
|
|
//currently in playback mode or not.
|
|
|
|
bool busy = gAudioIO->IsBusy();
|
|
if (!busy)
|
|
{
|
|
//If we aren't currently playing back, start playing back at
|
|
//the clicked point
|
|
ControlToolBar * ctb = p->GetControlToolBar();
|
|
//ctb->SetPlay(true);// Not needed as done in PlayPlayRegion
|
|
ctb->PlayPlayRegion
|
|
(SelectedRegion(clicktime, endtime), p->GetDefaultPlayOptions());
|
|
}
|
|
else
|
|
{
|
|
//If we are playing back, stop and move playback
|
|
//to the clicked point.
|
|
//This unpauses paused audio as well. The right thing to do might be to
|
|
//leave it paused but move the point. This would probably
|
|
//require a new method in ControlToolBar: SetPause();
|
|
ControlToolBar * ctb = p->GetControlToolBar();
|
|
ctb->StopPlaying();
|
|
ctb->PlayPlayRegion(SelectedRegion(clicktime, endtime), p->GetDefaultPlayOptions());
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
|
|
#ifdef EXPERIMENTAL_SCRUBBING_SMOOTH_SCROLL
|
|
double TrackPanel::FindScrubSpeed(double timeAtMouse) const
|
|
{
|
|
// Map a time (which was mapped from a mouse position)
|
|
// to a speed.
|
|
// Map times to positive and negative speeds,
|
|
// with the time at the midline of the screen mapping to 0,
|
|
// and the extremes to the maximum scrub speed.
|
|
|
|
// Width of visible track area, in time terms:
|
|
const double screen = mViewInfo->screen;
|
|
const double origin = mViewInfo->h + screen / 2.0;
|
|
|
|
// There are various snapping zones that are this fraction of screen:
|
|
const double snap = 0.05;
|
|
|
|
// By shrinking denom a bit, we make margins left and right
|
|
// that snap to maximum and negative maximum speeds.
|
|
const double factor = 1.0 - (snap * 2);
|
|
const double denom = factor * screen / 2.0;
|
|
double fraction = std::min(1.0, fabs(timeAtMouse - origin) / denom);
|
|
|
|
// Snap to 1.0 and -1.0
|
|
const double unity = 1.0 / mMaxScrubSpeed;
|
|
const double tolerance = snap / factor;
|
|
// Make speeds near 1 available too by remapping fractions outside
|
|
// this snap zone
|
|
if (fraction <= unity - tolerance)
|
|
fraction *= unity / (unity - tolerance);
|
|
else if (fraction < unity + tolerance)
|
|
fraction = unity;
|
|
else
|
|
fraction = unity + (fraction - (unity + tolerance)) *
|
|
(1.0 - unity) / (1.0 - (unity + tolerance));
|
|
|
|
double result = fraction * mMaxScrubSpeed;
|
|
if (timeAtMouse < origin)
|
|
result *= -1.0;
|
|
return result;
|
|
}
|
|
|
|
double TrackPanel::FindSeekSpeed(double timeAtMouse) const
|
|
{
|
|
// Map a time (which was mapped from a mouse position)
|
|
// to a signed skip speed: a multiplier of the stutter duration,
|
|
// by which to advance the play position.
|
|
// (The stutter will play at unit speed.)
|
|
|
|
// Times near the midline of the screen map to skip-less play,
|
|
// and the extremes to a value proportional to maximum scrub speed.
|
|
|
|
// If the maximum scrubbing speed defaults to 1.0 when you begin to scroll-scrub,
|
|
// the extreme skipping for scroll-seek needs to be larger to be useful.
|
|
static const double ARBITRARY_MULTIPLIER = 10.0;
|
|
const double extreme = std::max(1.0, mMaxScrubSpeed * ARBITRARY_MULTIPLIER);
|
|
|
|
// Width of visible track area, in time terms:
|
|
const double screen = mViewInfo->screen;
|
|
const double halfScreen = screen / 2.0;
|
|
const double origin = mViewInfo->h + halfScreen;
|
|
|
|
// The snapping zone is this fraction of screen, on each side of the
|
|
// center line:
|
|
const double snap = 0.05;
|
|
const double fraction =
|
|
std::max(snap, std::min(1.0, fabs(timeAtMouse - origin) / halfScreen));
|
|
|
|
double result = 1.0 + ((fraction - snap) / (1.0 - snap)) * (extreme - 1.0);
|
|
if (timeAtMouse < origin)
|
|
result *= -1.0;
|
|
return result;
|
|
}
|
|
#endif
|
|
|
|
#ifdef EXPERIMENTAL_SCRUBBING_BASIC
|
|
bool TrackPanel::PollIsSeeking()
|
|
{
|
|
return ::wxGetMouseState().LeftIsDown();
|
|
}
|
|
|
|
bool TrackPanel::IsScrubbing()
|
|
{
|
|
if (mScrubToken <= 0)
|
|
return false;
|
|
else if (mScrubToken == GetProject()->GetAudioIOToken())
|
|
return true;
|
|
else {
|
|
mScrubToken = -1;
|
|
mScrubStartPosition = -1;
|
|
#ifdef EXPERIMENTAL_SCRUBBING_SMOOTH_SCROLL
|
|
mSmoothScrollingScrub = false;
|
|
#endif
|
|
return false;
|
|
}
|
|
}
|
|
|
|
void TrackPanel::MarkScrubStart(
|
|
wxCoord xx
|
|
#ifdef EXPERIMENTAL_SCRUBBING_SMOOTH_SCROLL
|
|
, bool smoothScrolling
|
|
#endif
|
|
)
|
|
{
|
|
// Don't actually start scrubbing, but collect some information
|
|
// needed for the decision to start scrubbing later when handling
|
|
// drag events.
|
|
#ifdef EXPERIMENTAL_SCRUBBING_SMOOTH_SCROLL
|
|
mSmoothScrollingScrub = smoothScrolling;
|
|
#endif
|
|
mScrubStartPosition = xx;
|
|
mScrubStartClockTimeMillis = ::wxGetLocalTimeMillis();
|
|
}
|
|
|
|
bool TrackPanel::MaybeStartScrubbing(wxMouseEvent &event)
|
|
{
|
|
if (IsScrubbing())
|
|
return false;
|
|
else if (mScrubStartPosition >= 0) {
|
|
const bool busy = gAudioIO->IsBusy();
|
|
if (busy && gAudioIO->GetNumCaptureChannels() > 0) {
|
|
// Do not stop recording, and don't try to start scrubbing after
|
|
// recording stops
|
|
mScrubStartPosition = -1;
|
|
return false;
|
|
}
|
|
|
|
wxCoord position = event.m_x;
|
|
AudacityProject *p = GetActiveProject();
|
|
if (p &&
|
|
abs(mScrubStartPosition - position) >= SCRUBBING_PIXEL_TOLERANCE) {
|
|
ControlToolBar * ctb = p->GetControlToolBar();
|
|
double maxTime = p->GetTracks()->GetEndTime();
|
|
double time0 = std::min(maxTime,
|
|
mViewInfo->PositionToTime(mScrubStartPosition, GetLeftOffset())
|
|
);
|
|
double time1 = std::min(maxTime,
|
|
mViewInfo->PositionToTime(position, GetLeftOffset())
|
|
);
|
|
if (time1 != time0)
|
|
{
|
|
if (busy)
|
|
ctb->StopPlaying();
|
|
|
|
AudioIOStartStreamOptions options(p->GetDefaultPlayOptions());
|
|
options.timeTrack = NULL;
|
|
options.scrubDelay = (kTimerInterval / 1000.0);
|
|
options.scrubStartClockTimeMillis = mScrubStartClockTimeMillis;
|
|
options.minScrubStutter = 0.2;
|
|
#if 0
|
|
// Take the starting speed limit from the transcription toolbar,
|
|
// but it may be varied during the scrub.
|
|
mMaxScrubSpeed = options.maxScrubSpeed =
|
|
p->GetTranscriptionToolBar()->GetPlaySpeed();
|
|
#else
|
|
// That idea seems unpopular... just make it one
|
|
mMaxScrubSpeed = options.maxScrubSpeed = 1.0;
|
|
#endif
|
|
options.maxScrubTime = mTracks->GetEndTime();
|
|
const bool cutPreview = false;
|
|
const bool backwards = time1 < time0;
|
|
#ifdef EXPERIMENTAL_SCRUBBING_SCROLL_WHEEL
|
|
static const double maxScrubSpeedBase =
|
|
pow(2.0, 1.0 / ScrubSpeedStepsPerOctave);
|
|
mLogMaxScrubSpeed = floor(0.5 +
|
|
log(mMaxScrubSpeed) / log(maxScrubSpeedBase)
|
|
);
|
|
#endif
|
|
mScrubSpeedDisplayCountdown = 0;
|
|
mScrubToken =
|
|
ctb->PlayPlayRegion(SelectedRegion(time0, time1), options, cutPreview, backwards);
|
|
}
|
|
}
|
|
else
|
|
// Wait to test again
|
|
mScrubStartClockTimeMillis = ::wxGetLocalTimeMillis();
|
|
|
|
if (IsScrubbing()) {
|
|
mScrubHasFocus = true;
|
|
//mMouseCapture = IsMiddleButtonScrubbing;
|
|
//CaptureMouse();
|
|
}
|
|
return IsScrubbing();
|
|
}
|
|
else
|
|
return false;
|
|
}
|
|
|
|
bool TrackPanel::ContinueScrubbing(wxCoord position, bool hasFocus, bool seek)
|
|
{
|
|
// When we don't have focus, enqueue silent scrubs until we regain focus.
|
|
if (!hasFocus)
|
|
return gAudioIO->EnqueueScrubBySignedSpeed(0, mMaxScrubSpeed, false);
|
|
|
|
const double time = mViewInfo->PositionToTime(position, GetLeftOffset());
|
|
|
|
if (seek)
|
|
// Cause OnTimer() to suppress the speed display
|
|
mScrubSpeedDisplayCountdown = 1;
|
|
|
|
#ifdef EXPERIMENTAL_SCRUBBING_SMOOTH_SCROLL
|
|
if (mSmoothScrollingScrub) {
|
|
const double speed = seek ? FindSeekSpeed(time) : FindScrubSpeed(time);
|
|
return gAudioIO->EnqueueScrubBySignedSpeed(speed, mMaxScrubSpeed, seek);
|
|
}
|
|
else
|
|
#endif
|
|
return gAudioIO->EnqueueScrubByPosition
|
|
(time, seek ? 1.0 : mMaxScrubSpeed, seek);
|
|
}
|
|
|
|
bool TrackPanel::StopScrubbing()
|
|
{
|
|
if (IsScrubbing())
|
|
{
|
|
if (gAudioIO->IsBusy()) {
|
|
AudacityProject *p = GetActiveProject();
|
|
if (p) {
|
|
ControlToolBar * ctb = p->GetControlToolBar();
|
|
ctb->StopPlaying();
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
else
|
|
return false;
|
|
}
|
|
#endif
|
|
|
|
|
|
/// This method gets called when we're handling selection
|
|
/// and the mouse was just clicked.
|
|
void TrackPanel::SelectionHandleClick(wxMouseEvent & event,
|
|
Track * pTrack, wxRect r)
|
|
{
|
|
Track *rightTrack = NULL;
|
|
mCapturedTrack = pTrack;
|
|
mCapturedRect = r;
|
|
|
|
mMouseCapture=IsSelecting;
|
|
mInitialSelection = mViewInfo->selectedRegion;
|
|
|
|
// Save initial state of track selections, also,
|
|
// if the shift button is down and no track is selected yet,
|
|
// at least select the track we clicked into.
|
|
bool isAtLeastOneTrackSelected = false;
|
|
mInitialTrackSelection->clear();
|
|
{
|
|
bool nextTrackIsLinkFromPTrack = false;
|
|
TrackListIterator iter(mTracks);
|
|
for (Track *t = iter.First(); t; t = iter.Next()) {
|
|
const bool isSelected = t->GetSelected();
|
|
mInitialTrackSelection->push_back(isSelected);
|
|
if (isSelected) {
|
|
isAtLeastOneTrackSelected = true;
|
|
}
|
|
if (!isAtLeastOneTrackSelected) {
|
|
if (t == pTrack && t->GetLinked()) {
|
|
nextTrackIsLinkFromPTrack = true;
|
|
}
|
|
else if (nextTrackIsLinkFromPTrack) {
|
|
rightTrack = t;
|
|
nextTrackIsLinkFromPTrack = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// We create a new snap manager in case any snap-points have changed
|
|
if (mSnapManager)
|
|
delete mSnapManager;
|
|
|
|
mSnapManager = new SnapManager(mTracks, NULL,
|
|
*mViewInfo,
|
|
4); // pixel tolerance
|
|
|
|
mSnapLeft = -1;
|
|
mSnapRight = -1;
|
|
|
|
#ifdef USE_MIDI
|
|
mStretching = false;
|
|
bool stretch = HitTestStretch(pTrack, r, event);
|
|
#endif
|
|
|
|
if (event.ShiftDown()
|
|
|
|
#ifdef USE_MIDI
|
|
&& !stretch
|
|
#endif
|
|
) {
|
|
if (!isAtLeastOneTrackSelected) {
|
|
pTrack->SetSelected(true);
|
|
if (rightTrack)
|
|
rightTrack->SetSelected(true);
|
|
else if (pTrack->GetLink())
|
|
pTrack->GetLink()->SetSelected(true);
|
|
}
|
|
|
|
double value;
|
|
// Shift-click, choose closest boundary
|
|
SelectionBoundary boundary =
|
|
ChooseBoundary(event, pTrack, r, false, false, &value);
|
|
switch (boundary) {
|
|
case SBLeft:
|
|
case SBRight:
|
|
{
|
|
#ifdef EXPERIMENTAL_SPECTRAL_EDITING
|
|
// If drag starts, change time selection only
|
|
// (also exit frequency snapping)
|
|
mFreqSelMode = FREQ_SEL_INVALID;
|
|
#endif
|
|
mSelStartValid = true;
|
|
mSelStart = value;
|
|
ExtendSelection(event.m_x, r.x, pTrack);
|
|
break;
|
|
}
|
|
#ifdef EXPERIMENTAL_SPECTRAL_EDITING
|
|
case SBBottom:
|
|
case SBTop:
|
|
{
|
|
mFreqSelTrack = static_cast<const WaveTrack*>(pTrack);
|
|
mFreqSelPin = value;
|
|
mFreqSelMode =
|
|
(boundary == SBBottom)
|
|
? FREQ_SEL_BOTTOM_FREE : FREQ_SEL_TOP_FREE;
|
|
|
|
// Drag frequency only, not time:
|
|
mSelStartValid = false;
|
|
ExtendFreqSelection(event.m_y, r.y, r.height);
|
|
break;
|
|
}
|
|
case SBCenter:
|
|
HandleCenterFrequencyClick(true, pTrack, value);
|
|
break;
|
|
#endif
|
|
default:
|
|
wxASSERT(false);
|
|
};
|
|
|
|
UpdateSelectionDisplay();
|
|
// For persistence of the selection change:
|
|
MakeParentModifyState(false);
|
|
return;
|
|
}
|
|
|
|
else if(event.CmdDown()
|
|
#ifdef USE_MIDI
|
|
&& !stretch
|
|
#endif
|
|
) {
|
|
|
|
#ifdef EXPERIMENTAL_SCRUBBING_BASIC
|
|
if (
|
|
#ifdef EXPERIMENTAL_SCRUBBING_SMOOTH_SCROLL
|
|
event.LeftDClick() ||
|
|
#endif
|
|
event.LeftDown()) {
|
|
MarkScrubStart(
|
|
event.m_x
|
|
#ifdef EXPERIMENTAL_SCRUBBING_SMOOTH_SCROLL
|
|
, event.LeftDClick()
|
|
#endif
|
|
);
|
|
return;
|
|
}
|
|
|
|
#else
|
|
|
|
StartOrJumpPlayback(event);
|
|
|
|
#endif
|
|
|
|
// Not starting a drag
|
|
SetCapturedTrack(NULL, IsUncaptured);
|
|
return;
|
|
}
|
|
|
|
//Make sure you are within the selected track
|
|
bool startNewSelection = true;
|
|
if (pTrack && pTrack->GetSelected()) {
|
|
// Adjusting selection edges can be turned off in the
|
|
// preferences now
|
|
if (mAdjustSelectionEdges) {
|
|
#ifdef EXPERIMENTAL_SPECTRAL_EDITING
|
|
if (mFreqSelMode == FREQ_SEL_SNAPPING_CENTER &&
|
|
isSpectralSelectionTrack(pTrack)) {
|
|
// Ignore whether we are inside the time selection.
|
|
// Exit center-snapping, start dragging the width.
|
|
mFreqSelMode = FREQ_SEL_PINNED_CENTER;
|
|
mFreqSelTrack = static_cast<WaveTrack*>(pTrack);
|
|
mFreqSelPin = mViewInfo->selectedRegion.fc();
|
|
// Do not adjust time boundaries
|
|
mSelStartValid = false;
|
|
ExtendFreqSelection(event.m_y, r.y, r.height);
|
|
UpdateSelectionDisplay();
|
|
// Frequency selection doesn't persist (yet?), so skip this:
|
|
// MakeParentModifyState(false);
|
|
|
|
return;
|
|
}
|
|
else
|
|
#endif
|
|
{
|
|
// Not shift-down, choose boundary only within snapping
|
|
double value;
|
|
SelectionBoundary boundary =
|
|
ChooseBoundary(event, pTrack, r, true, true, &value);
|
|
switch (boundary) {
|
|
case SBNone:
|
|
// startNewSelection remains true
|
|
break;
|
|
case SBLeft:
|
|
case SBRight:
|
|
startNewSelection = false;
|
|
#ifdef EXPERIMENTAL_SPECTRAL_EDITING
|
|
// Disable frequency selection
|
|
mFreqSelMode = FREQ_SEL_INVALID;
|
|
#endif
|
|
mSelStartValid = true;
|
|
mSelStart = value;
|
|
break;
|
|
#ifdef EXPERIMENTAL_SPECTRAL_EDITING
|
|
case SBBottom:
|
|
case SBTop:
|
|
case SBWidth:
|
|
startNewSelection = false;
|
|
// Disable time selection
|
|
mSelStartValid = false;
|
|
mFreqSelTrack = static_cast<const WaveTrack*>(pTrack);
|
|
mFreqSelPin = value;
|
|
mFreqSelMode =
|
|
(boundary == SBWidth) ? FREQ_SEL_PINNED_CENTER :
|
|
(boundary == SBBottom) ? FREQ_SEL_BOTTOM_FREE :
|
|
FREQ_SEL_TOP_FREE;
|
|
break;
|
|
case SBCenter:
|
|
HandleCenterFrequencyClick(false, pTrack, value);
|
|
startNewSelection = false;
|
|
break;
|
|
#endif
|
|
default:
|
|
wxASSERT(false);
|
|
}
|
|
}
|
|
} // mAdjustSelectionEdges
|
|
}
|
|
|
|
//Determine if user clicked on a label track.
|
|
if (pTrack && (pTrack->GetKind() == Track::Label))
|
|
{
|
|
LabelTrack *lt = (LabelTrack *) pTrack;
|
|
if (lt->HandleMouse(event, r,//mCapturedRect,
|
|
*mViewInfo,
|
|
&mViewInfo->selectedRegion)) {
|
|
MakeParentPushState(_("Modified Label"),
|
|
_("Label Edit"),
|
|
PUSH_CONSOLIDATE);
|
|
}
|
|
|
|
// IF the user clicked a label, THEN select all other tracks by Label
|
|
if (lt->IsSelected()) {
|
|
mTracks->Select(lt);
|
|
SelectTracksByLabel( lt );
|
|
DisplaySelection();
|
|
|
|
// Not starting a drag
|
|
SetCapturedTrack(NULL, IsUncaptured);
|
|
return;
|
|
}
|
|
}
|
|
|
|
#ifdef USE_MIDI
|
|
if (stretch) {
|
|
NoteTrack *nt = (NoteTrack *) pTrack;
|
|
// find nearest beat to sel0, sel1
|
|
double minPeriod = 0.05; // minimum beat period
|
|
double qBeat0, qBeat1;
|
|
double centerBeat = 0.0f;
|
|
mStretchSel0 = nt->NearestBeatTime(mViewInfo->selectedRegion.t0(), &qBeat0);
|
|
mStretchSel1 = nt->NearestBeatTime(mViewInfo->selectedRegion.t1(), &qBeat1);
|
|
|
|
// If there is not (almost) a beat to stretch that is slower
|
|
// than 20 beats per second, don't stretch
|
|
if (within(qBeat0, qBeat1, 0.9) ||
|
|
(mStretchSel1 - mStretchSel0) / (qBeat1 - qBeat0) < minPeriod) return;
|
|
|
|
if (startNewSelection) { // mouse is not at an edge, but after
|
|
// quantization, we could be indicating the selection edge
|
|
mSelStartValid = true;
|
|
mSelStart = std::max(0.0, mViewInfo->PositionToTime(event.m_x, r.x));
|
|
mStretchStart = nt->NearestBeatTime(mSelStart, ¢erBeat);
|
|
if (within(qBeat0, centerBeat, 0.1)) {
|
|
mListener->TP_DisplayStatusMessage(
|
|
_("Click and drag to stretch selected region."));
|
|
SetCursor(*mStretchLeftCursor);
|
|
// mStretchMode = stretchLeft;
|
|
mSelStart = mViewInfo->selectedRegion.t1();
|
|
// condition that implies stretchLeft
|
|
startNewSelection = false;
|
|
} else if (within(qBeat1, centerBeat, 0.1)) {
|
|
mListener->TP_DisplayStatusMessage(
|
|
_("Click and drag to stretch selected region."));
|
|
SetCursor(*mStretchRightCursor);
|
|
// mStretchMode = stretchRight;
|
|
mSelStart = mViewInfo->selectedRegion.t0();
|
|
// condition that implies stretchRight
|
|
startNewSelection = false;
|
|
}
|
|
}
|
|
|
|
if (startNewSelection) {
|
|
mStretchMode = stretchCenter;
|
|
mStretchLeftBeats = qBeat1 - centerBeat;
|
|
mStretchRightBeats = centerBeat - qBeat0;
|
|
} else if (mSelStartValid && mViewInfo->selectedRegion.t1() == mSelStart) {
|
|
// note that at this point, mSelStart is at the opposite
|
|
// end of the selection from the cursor. If the cursor is
|
|
// over sel0, then mSelStart is at sel1.
|
|
mStretchMode = stretchLeft;
|
|
} else {
|
|
mStretchMode = stretchRight;
|
|
}
|
|
|
|
if (mStretchMode == stretchLeft) {
|
|
mStretchLeftBeats = 0;
|
|
mStretchRightBeats = qBeat1 - qBeat0;
|
|
} else if (mStretchMode == stretchRight) {
|
|
mStretchLeftBeats = qBeat1 - qBeat0;
|
|
mStretchRightBeats = 0;
|
|
}
|
|
mViewInfo->selectedRegion.setTimes(mStretchSel0, mStretchSel1);
|
|
mStretching = true;
|
|
mStretched = false;
|
|
|
|
/* i18n-hint: (noun) The track that is used for MIDI notes which can be
|
|
dragged to change their duration.*/
|
|
MakeParentPushState(_("Stretch Note Track"),
|
|
/* i18n-hint: In the history list, indicates a MIDI note has
|
|
been dragged to change its duration (stretch it). Using either past
|
|
or present tense is fine here. If unsure, go for whichever is
|
|
shorter.*/
|
|
_("Stretch"));
|
|
|
|
// Full refresh since the label area may need to indicate
|
|
// newly selected tracks. (I'm really not sure if the label area
|
|
// needs to be refreshed or how to just refresh non-label areas.-RBD)
|
|
Refresh(false);
|
|
|
|
// Make sure the ruler follows suit.
|
|
mRuler->DrawSelection();
|
|
|
|
// As well as the SelectionBar.
|
|
DisplaySelection();
|
|
|
|
return;
|
|
}
|
|
#endif
|
|
if (startNewSelection) {
|
|
// If we didn't move a selection boundary, start a new selection
|
|
SelectNone();
|
|
#ifdef EXPERIMENTAL_SPECTRAL_EDITING
|
|
StartFreqSelection (event.m_y, r.y, r.height, pTrack);
|
|
#endif
|
|
StartSelection(event.m_x, r.x);
|
|
mTracks->Select(pTrack);
|
|
SetFocusedTrack(pTrack);
|
|
//On-Demand: check to see if there is an OD thing associated with this track.
|
|
if (pTrack->GetKind() == Track::Wave) {
|
|
if(ODManager::IsInstanceCreated())
|
|
ODManager::Instance()->DemandTrackUpdate((WaveTrack*)pTrack,mSelStart);
|
|
}
|
|
DisplaySelection();
|
|
}
|
|
}
|
|
|
|
|
|
/// Reset our selection markers.
|
|
void TrackPanel::StartSelection(int mouseXCoordinate, int trackLeftEdge)
|
|
{
|
|
mSelStartValid = true;
|
|
mSelStart = std::max(0.0, mViewInfo->PositionToTime(mouseXCoordinate, trackLeftEdge));
|
|
|
|
double s = mSelStart;
|
|
|
|
if (mSnapManager) {
|
|
mSnapLeft = -1;
|
|
mSnapRight = -1;
|
|
bool snappedPoint, snappedTime;
|
|
if (mSnapManager->Snap(mCapturedTrack, mSelStart, false,
|
|
&s, &snappedPoint, &snappedTime)) {
|
|
if (snappedPoint)
|
|
mSnapLeft = mViewInfo->TimeToPosition(s, trackLeftEdge);
|
|
}
|
|
}
|
|
|
|
mViewInfo->selectedRegion.setTimes(s, s);
|
|
|
|
SonifyBeginModifyState();
|
|
MakeParentModifyState(false);
|
|
SonifyEndModifyState();
|
|
}
|
|
|
|
/// Extend the existing selection
|
|
void TrackPanel::ExtendSelection(int mouseXCoordinate, int trackLeftEdge,
|
|
Track *pTrack)
|
|
{
|
|
if (!mSelStartValid)
|
|
// Must be dragging frequency bounds only.
|
|
return;
|
|
|
|
double selend = std::max(0.0, mViewInfo->PositionToTime(mouseXCoordinate, trackLeftEdge));
|
|
clip_bottom(selend, 0.0);
|
|
|
|
double origSel0, origSel1;
|
|
double sel0, sel1;
|
|
|
|
if (pTrack == NULL && mCapturedTrack != NULL)
|
|
pTrack = mCapturedTrack;
|
|
|
|
if (mSelStart < selend) {
|
|
sel0 = mSelStart;
|
|
sel1 = selend;
|
|
}
|
|
else {
|
|
sel1 = mSelStart;
|
|
sel0 = selend;
|
|
}
|
|
|
|
origSel0 = sel0;
|
|
origSel1 = sel1;
|
|
|
|
if (mSnapManager) {
|
|
mSnapLeft = -1;
|
|
mSnapRight = -1;
|
|
bool snappedPoint, snappedTime;
|
|
if (mSnapManager->Snap(mCapturedTrack, sel0, false,
|
|
&sel0, &snappedPoint, &snappedTime)) {
|
|
if (snappedPoint)
|
|
mSnapLeft = mViewInfo->TimeToPosition(sel0, trackLeftEdge);
|
|
}
|
|
if (mSnapManager->Snap(mCapturedTrack, sel1, true,
|
|
&sel1, &snappedPoint, &snappedTime)) {
|
|
if (snappedPoint)
|
|
mSnapRight = mViewInfo->TimeToPosition(sel1, trackLeftEdge);
|
|
}
|
|
|
|
// Check if selection endpoints are too close together to snap (unless
|
|
// using snap-to-time -- then we always accept the snap results)
|
|
if (mSnapLeft >= 0 && mSnapRight >= 0 && mSnapRight - mSnapLeft < 3 &&
|
|
!snappedTime) {
|
|
sel0 = origSel0;
|
|
sel1 = origSel1;
|
|
mSnapLeft = -1;
|
|
mSnapRight = -1;
|
|
}
|
|
}
|
|
|
|
mViewInfo->selectedRegion.setTimes(sel0, sel1);
|
|
|
|
//On-Demand: check to see if there is an OD thing associated with this track. If so we want to update the focal point for the task.
|
|
if (pTrack && (pTrack->GetKind() == Track::Wave) && ODManager::IsInstanceCreated())
|
|
ODManager::Instance()->DemandTrackUpdate((WaveTrack*)pTrack,sel0); //sel0 is sometimes less than mSelStart
|
|
}
|
|
|
|
void TrackPanel::UpdateSelectionDisplay()
|
|
{
|
|
// Full refresh since the label area may need to indicate
|
|
// newly selected tracks.
|
|
Refresh(false);
|
|
|
|
// Make sure the ruler follows suit.
|
|
mRuler->DrawSelection();
|
|
|
|
// As well as the SelectionBar.
|
|
DisplaySelection();
|
|
}
|
|
|
|
#ifdef EXPERIMENTAL_SPECTRAL_EDITING
|
|
namespace {
|
|
|
|
inline double findMaxRatio(double center, double rate)
|
|
{
|
|
const double minFrequency = 1.0;
|
|
const double maxFrequency = (rate / 2.0);
|
|
const double frequency =
|
|
std::min(maxFrequency,
|
|
std::max(minFrequency, center));
|
|
return
|
|
std::min(frequency / minFrequency, maxFrequency / frequency);
|
|
}
|
|
|
|
}
|
|
|
|
void TrackPanel::SnapCenterOnce(const WaveTrack *pTrack, bool up)
|
|
{
|
|
const SpectrogramSettings &settings = pTrack->GetSpectrogramSettings();
|
|
const int windowSize = settings.GetFFTLength();
|
|
const double rate = pTrack->GetRate();
|
|
const double nyq = rate / 2.0;
|
|
const double binFrequency = rate / windowSize;
|
|
|
|
double f1 = mViewInfo->selectedRegion.f1();
|
|
double centerFrequency = mViewInfo->selectedRegion.fc();
|
|
if (centerFrequency <= 0) {
|
|
centerFrequency = up ? binFrequency : nyq;
|
|
f1 = centerFrequency * sqrt(2.0);
|
|
}
|
|
|
|
const double ratio = f1 / centerFrequency;
|
|
const int originalBin = floor(0.5 + centerFrequency / binFrequency);
|
|
const int limitingBin = up ? floor(0.5 + nyq / binFrequency) : 1;
|
|
|
|
// This is crude and wasteful, doing the FFT each time the command is called.
|
|
// It would be better to cache the data, but then invalidation of the cache would
|
|
// need doing in all places that change the time selection.
|
|
StartSnappingFreqSelection(pTrack);
|
|
double snappedFrequency = centerFrequency;
|
|
int bin = originalBin;
|
|
if (up) {
|
|
while (snappedFrequency <= centerFrequency &&
|
|
bin < limitingBin)
|
|
snappedFrequency = mFrequencySnapper->FindPeak(++bin * binFrequency, NULL);
|
|
}
|
|
else {
|
|
while (snappedFrequency >= centerFrequency &&
|
|
bin > limitingBin)
|
|
snappedFrequency = mFrequencySnapper->FindPeak(--bin * binFrequency, NULL);
|
|
}
|
|
|
|
mViewInfo->selectedRegion.setFrequencies
|
|
(snappedFrequency / ratio, snappedFrequency * ratio);
|
|
}
|
|
|
|
void TrackPanel::StartSnappingFreqSelection (const WaveTrack *pTrack)
|
|
{
|
|
static const sampleCount minLength = 8;
|
|
|
|
const double rate = pTrack->GetRate();
|
|
|
|
// Grab samples, just for this track, at these times
|
|
std::vector<float> frequencySnappingData;
|
|
const sampleCount start =
|
|
pTrack->TimeToLongSamples(mViewInfo->selectedRegion.t0());
|
|
const sampleCount end =
|
|
pTrack->TimeToLongSamples(mViewInfo->selectedRegion.t1());
|
|
const sampleCount length =
|
|
std::min(sampleCount(frequencySnappingData.max_size()),
|
|
std::min(sampleCount(10485760), // as in FreqWindow.cpp
|
|
end - start));
|
|
const sampleCount effectiveLength = std::max(minLength, length);
|
|
frequencySnappingData.resize(effectiveLength, 0.0f);
|
|
pTrack->Get(
|
|
reinterpret_cast<samplePtr>(&frequencySnappingData[0]),
|
|
floatSample, start, length);
|
|
|
|
// Use same settings as are now used for spectrogram display,
|
|
// except, shrink the window as needed so we get some answers
|
|
|
|
const SpectrogramSettings &settings = pTrack->GetSpectrogramSettings();
|
|
int windowSize = settings.GetFFTLength();
|
|
|
|
while(windowSize > effectiveLength)
|
|
windowSize >>= 1;
|
|
const int windowType = settings.windowType;
|
|
|
|
mFrequencySnapper->Calculate(
|
|
SpectrumAnalyst::Spectrum, windowType, windowSize, rate,
|
|
&frequencySnappingData[0], length);
|
|
|
|
// We can now throw away the sample data but we keep the spectrum.
|
|
}
|
|
|
|
void TrackPanel::MoveSnappingFreqSelection (int mouseYCoordinate,
|
|
int trackTopEdge,
|
|
int trackHeight, Track *pTrack)
|
|
{
|
|
if (pTrack &&
|
|
pTrack->GetSelected() &&
|
|
isSpectralSelectionTrack(pTrack)) {
|
|
WaveTrack *const wt = static_cast<WaveTrack*>(pTrack);
|
|
// PRL:
|
|
// What happens if center snapping selection began in one spectrogram track,
|
|
// then continues inside another? We do not then recalculate
|
|
// the spectrum (as was done in StartSnappingFreqSelection)
|
|
// but snap according to the peaks in the old track.
|
|
// I am not worrying about that odd case.
|
|
const double rate = wt->GetRate();
|
|
const double frequency =
|
|
PositionToFrequency(wt, false, mouseYCoordinate,
|
|
trackTopEdge, trackHeight);
|
|
const double snappedFrequency =
|
|
mFrequencySnapper->FindPeak(frequency, NULL);
|
|
const double maxRatio = findMaxRatio(snappedFrequency, rate);
|
|
double ratio = 2.0; // An arbitrary octave on each side, at most
|
|
{
|
|
const double f0 = mViewInfo->selectedRegion.f0();
|
|
const double f1 = mViewInfo->selectedRegion.f1();
|
|
if (f1 >= f0 && f0 >= 0)
|
|
// Preserve already chosen ratio instead
|
|
ratio = sqrt(f1 / f0);
|
|
}
|
|
ratio = std::min(ratio, maxRatio);
|
|
|
|
mFreqSelPin = snappedFrequency;
|
|
mViewInfo->selectedRegion.setFrequencies(
|
|
snappedFrequency / ratio, snappedFrequency * ratio);
|
|
|
|
mFreqSelTrack = wt;
|
|
// SelectNone();
|
|
// mTracks->Select(pTrack);
|
|
SetFocusedTrack(pTrack);
|
|
}
|
|
}
|
|
|
|
void TrackPanel::StartFreqSelection (int mouseYCoordinate, int trackTopEdge,
|
|
int trackHeight, Track *pTrack)
|
|
{
|
|
mFreqSelTrack = 0;
|
|
mFreqSelMode = FREQ_SEL_INVALID;
|
|
mFreqSelPin = SelectedRegion::UndefinedFrequency;
|
|
|
|
if (isSpectralSelectionTrack(pTrack)) {
|
|
mFreqSelTrack = static_cast<WaveTrack*>(pTrack);
|
|
mFreqSelMode = FREQ_SEL_FREE;
|
|
mFreqSelPin =
|
|
PositionToFrequency(mFreqSelTrack, false, mouseYCoordinate,
|
|
trackTopEdge, trackHeight);
|
|
mViewInfo->selectedRegion.setFrequencies(mFreqSelPin, mFreqSelPin);
|
|
}
|
|
}
|
|
|
|
void TrackPanel::ExtendFreqSelection(int mouseYCoordinate, int trackTopEdge,
|
|
int trackHeight)
|
|
{
|
|
// When dragWidth is true, and not dragging the center,
|
|
// adjust both top and bottom about geometric mean.
|
|
|
|
if (mFreqSelMode == FREQ_SEL_INVALID ||
|
|
mFreqSelMode == FREQ_SEL_SNAPPING_CENTER)
|
|
return;
|
|
|
|
// Extension happens only when dragging in the same track in which we
|
|
// started, and that is of a spectrogram display type.
|
|
|
|
const WaveTrack* wt = mFreqSelTrack;
|
|
const double rate = wt->GetRate();
|
|
const double frequency =
|
|
PositionToFrequency(wt, true, mouseYCoordinate,
|
|
trackTopEdge, trackHeight);
|
|
|
|
// Dragging center?
|
|
if (mFreqSelMode == FREQ_SEL_DRAG_CENTER) {
|
|
if (frequency == rate || frequency < 1.0)
|
|
// snapped to top or bottom
|
|
mViewInfo->selectedRegion.setFrequencies(
|
|
SelectedRegion::UndefinedFrequency,
|
|
SelectedRegion::UndefinedFrequency);
|
|
else {
|
|
// mFreqSelPin holds the ratio of top to center
|
|
const double maxRatio = findMaxRatio(frequency, rate);
|
|
const double ratio = std::min(maxRatio, mFreqSelPin);
|
|
mViewInfo->selectedRegion.setFrequencies(
|
|
frequency / ratio, frequency * ratio);
|
|
}
|
|
}
|
|
else if (mFreqSelMode == FREQ_SEL_PINNED_CENTER) {
|
|
if (mFreqSelPin >= 0) {
|
|
// Change both upper and lower edges leaving centre where it is.
|
|
if (frequency == rate || frequency < 1.0)
|
|
// snapped to top or bottom
|
|
mViewInfo->selectedRegion.setFrequencies(
|
|
SelectedRegion::UndefinedFrequency,
|
|
SelectedRegion::UndefinedFrequency);
|
|
else {
|
|
// Given center and mouse position, find ratio of the larger to the
|
|
// smaller, limit that to the frequency scale bounds, and adjust
|
|
// top and bottom accordingly.
|
|
const double maxRatio = findMaxRatio(mFreqSelPin, rate);
|
|
double ratio = frequency / mFreqSelPin;
|
|
if (ratio < 1.0)
|
|
ratio = 1.0 / ratio;
|
|
ratio = std::min(maxRatio, ratio);
|
|
mViewInfo->selectedRegion.setFrequencies(
|
|
mFreqSelPin / ratio, mFreqSelPin * ratio);
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
// Dragging of upper or lower.
|
|
const bool bottomDefined =
|
|
!(mFreqSelMode == FREQ_SEL_TOP_FREE && mFreqSelPin < 0);
|
|
const bool topDefined =
|
|
!(mFreqSelMode == FREQ_SEL_BOTTOM_FREE && mFreqSelPin < 0);
|
|
if (!bottomDefined || (topDefined && mFreqSelPin < frequency)) {
|
|
// Adjust top
|
|
if (frequency == rate)
|
|
// snapped high; upper frequency is undefined
|
|
mViewInfo->selectedRegion.setF1(SelectedRegion::UndefinedFrequency);
|
|
else
|
|
mViewInfo->selectedRegion.setF1(std::max(1.0, frequency));
|
|
|
|
mViewInfo->selectedRegion.setF0(mFreqSelPin);
|
|
}
|
|
else {
|
|
// Adjust bottom
|
|
if (frequency < 1.0)
|
|
// snapped low; lower frequency is undefined
|
|
mViewInfo->selectedRegion.setF0(SelectedRegion::UndefinedFrequency);
|
|
else
|
|
mViewInfo->selectedRegion.setF0(std::min(rate / 2.0, frequency));
|
|
|
|
mViewInfo->selectedRegion.setF1(mFreqSelPin);
|
|
}
|
|
}
|
|
}
|
|
|
|
void TrackPanel::ToggleSpectralSelection()
|
|
{
|
|
SelectedRegion ®ion = mViewInfo->selectedRegion;
|
|
const double f0 = region.f0();
|
|
const double f1 = region.f1();
|
|
const bool haveSpectralSelection =
|
|
!(f0 == SelectedRegion::UndefinedFrequency &&
|
|
f1 == SelectedRegion::UndefinedFrequency);
|
|
if (haveSpectralSelection)
|
|
{
|
|
mLastF0 = f0;
|
|
mLastF1 = f1;
|
|
region.setFrequencies
|
|
(SelectedRegion::UndefinedFrequency, SelectedRegion::UndefinedFrequency);
|
|
}
|
|
else
|
|
region.setFrequencies(mLastF0, mLastF1);
|
|
}
|
|
|
|
void TrackPanel::ResetFreqSelectionPin(double hintFrequency, bool logF)
|
|
{
|
|
switch (mFreqSelMode) {
|
|
case FREQ_SEL_INVALID:
|
|
case FREQ_SEL_SNAPPING_CENTER:
|
|
mFreqSelPin = -1.0;
|
|
break;
|
|
|
|
case FREQ_SEL_PINNED_CENTER:
|
|
mFreqSelPin = mViewInfo->selectedRegion.fc();
|
|
break;
|
|
|
|
case FREQ_SEL_DRAG_CENTER:
|
|
{
|
|
// Re-pin the width
|
|
const double f0 = mViewInfo->selectedRegion.f0();
|
|
const double f1 = mViewInfo->selectedRegion.f1();
|
|
if (f0 >= 0 && f1 >= 0)
|
|
mFreqSelPin = sqrt(f1 / f0);
|
|
else
|
|
mFreqSelPin = -1.0;
|
|
}
|
|
break;
|
|
|
|
case FREQ_SEL_FREE:
|
|
// Pin which? Farther from the hint which is the presumed
|
|
// mouse position.
|
|
{
|
|
const double f0 = mViewInfo->selectedRegion.f0();
|
|
const double f1 = mViewInfo->selectedRegion.f1();
|
|
if (logF) {
|
|
if (f1 < 0)
|
|
mFreqSelPin = f0;
|
|
else {
|
|
const double logf1 = log(std::max(1.0, f1));
|
|
const double logf0 = log(std::max(1.0, f0));
|
|
const double logHint = log(std::max(1.0, hintFrequency));
|
|
if (abs (logHint - logf1) < abs (logHint - logf0))
|
|
mFreqSelPin = f0;
|
|
else
|
|
mFreqSelPin = f1;
|
|
}
|
|
}
|
|
else {
|
|
if (f1 < 0 ||
|
|
abs (hintFrequency - f1) < abs (hintFrequency - f0))
|
|
mFreqSelPin = f0;
|
|
else
|
|
mFreqSelPin = f1;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case FREQ_SEL_TOP_FREE:
|
|
mFreqSelPin = mViewInfo->selectedRegion.f0();
|
|
break;
|
|
|
|
case FREQ_SEL_BOTTOM_FREE:
|
|
mFreqSelPin = mViewInfo->selectedRegion.f1();
|
|
break;
|
|
|
|
default:
|
|
wxASSERT(false);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
#ifdef USE_MIDI
|
|
void TrackPanel::Stretch(int mouseXCoordinate, int trackLeftEdge,
|
|
Track *pTrack)
|
|
{
|
|
if (mStretched) { // Undo stretch and redo it with new mouse coordinates
|
|
// Drag handling was not originally implemented with Undo in mind --
|
|
// there are saved pointers to tracks that are not supposed to change.
|
|
// Undo will change tracks, so convert pTrack, mCapturedTrack to index
|
|
// values, then look them up after the Undo
|
|
TrackListIterator iter(mTracks);
|
|
int pTrackIndex = pTrack->GetIndex();
|
|
int capturedTrackIndex =
|
|
(mCapturedTrack ? mCapturedTrack->GetIndex() : 0);
|
|
|
|
GetProject()->OnUndo();
|
|
|
|
// Undo brings us back to the pre-click state, but we want to
|
|
// quantize selected region to integer beat boundaries. These
|
|
// were saved in mStretchSel[12] variables:
|
|
mViewInfo->selectedRegion.setTimes(mStretchSel0, mStretchSel1);
|
|
|
|
mStretched = false;
|
|
int index = 0;
|
|
for (Track *t = iter.First(mTracks); t; t = iter.Next()) {
|
|
if (index == pTrackIndex) pTrack = t;
|
|
if (mCapturedTrack && index == capturedTrackIndex) mCapturedTrack = t;
|
|
index++;
|
|
}
|
|
}
|
|
|
|
if (pTrack == NULL && mCapturedTrack != NULL)
|
|
pTrack = mCapturedTrack;
|
|
|
|
if (!pTrack || pTrack->GetKind() != Track::Note) {
|
|
return;
|
|
}
|
|
|
|
NoteTrack *pNt = (NoteTrack *) pTrack;
|
|
double moveto = std::max(0.0, mViewInfo->PositionToTime(mouseXCoordinate, trackLeftEdge));
|
|
|
|
// check to make sure tempo is not higher than 20 beats per second
|
|
// (In principle, tempo can be higher, but not infinity.)
|
|
double minPeriod = 0.05; // minimum beat period
|
|
double qBeat0, qBeat1;
|
|
pNt->NearestBeatTime(mViewInfo->selectedRegion.t0(), &qBeat0); // get beat
|
|
pNt->NearestBeatTime(mViewInfo->selectedRegion.t1(), &qBeat1);
|
|
|
|
// We could be moving 3 things: left edge, right edge, a point between
|
|
switch (mStretchMode) {
|
|
case stretchLeft: {
|
|
// make sure target duration is not too short
|
|
double dur = mViewInfo->selectedRegion.t1() - moveto;
|
|
if (dur < mStretchRightBeats * minPeriod) {
|
|
dur = mStretchRightBeats * minPeriod;
|
|
moveto = mViewInfo->selectedRegion.t1() - dur;
|
|
}
|
|
if (pNt->StretchRegion(mStretchSel0, mStretchSel1, dur)) {
|
|
pNt->SetOffset(pNt->GetOffset() + moveto - mStretchSel0);
|
|
mViewInfo->selectedRegion.setT0(moveto);
|
|
}
|
|
break;
|
|
}
|
|
case stretchRight: {
|
|
// make sure target duration is not too short
|
|
double dur = moveto - mViewInfo->selectedRegion.t0();
|
|
if (dur < mStretchLeftBeats * minPeriod) {
|
|
dur = mStretchLeftBeats * minPeriod;
|
|
moveto = mStretchSel0 + dur;
|
|
}
|
|
if (pNt->StretchRegion(mStretchSel0, mStretchSel1, dur)) {
|
|
mViewInfo->selectedRegion.setT1(moveto);
|
|
}
|
|
break;
|
|
}
|
|
case stretchCenter: {
|
|
// make sure both left and right target durations are not too short
|
|
double left_dur = moveto - mViewInfo->selectedRegion.t0();
|
|
double right_dur = mViewInfo->selectedRegion.t1() - moveto;
|
|
double centerBeat;
|
|
pNt->NearestBeatTime(mSelStart, ¢erBeat);
|
|
if (left_dur < mStretchLeftBeats * minPeriod) {
|
|
left_dur = mStretchLeftBeats * minPeriod;
|
|
moveto = mStretchSel0 + left_dur;
|
|
}
|
|
if (right_dur < mStretchRightBeats * minPeriod) {
|
|
right_dur = mStretchRightBeats * minPeriod;
|
|
moveto = mStretchSel1 - right_dur;
|
|
}
|
|
pNt->StretchRegion(mStretchStart, mStretchSel1, right_dur);
|
|
pNt->StretchRegion(mStretchSel0, mStretchStart, left_dur);
|
|
break;
|
|
}
|
|
default:
|
|
wxASSERT(false);
|
|
break;
|
|
}
|
|
MakeParentPushState(_("Stretch Note Track"), _("Stretch"),
|
|
PUSH_CONSOLIDATE | PUSH_AUTOSAVE);
|
|
mStretched = true;
|
|
Refresh(false);
|
|
}
|
|
#endif
|
|
|
|
/// AS: If we're dragging to extend a selection (or actually,
|
|
/// if the screen is scrolling while you're selecting), we
|
|
/// handle it here.
|
|
void TrackPanel::SelectionHandleDrag(wxMouseEvent & event, Track *clickedTrack)
|
|
{
|
|
#ifdef EXPERIMENTAL_SCRUBBING_BASIC
|
|
if (mScrubStartPosition >= 0) {
|
|
MaybeStartScrubbing(event);
|
|
// Do nothing more, don't change selection
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
// AS: If we're not in the process of selecting (set in
|
|
// the SelectionHandleClick above), fuhggeddaboudit.
|
|
if (mMouseCapture!=IsSelecting)
|
|
return;
|
|
|
|
// Also fuhggeddaboudit if we're not dragging and not autoscrolling.
|
|
if (!event.Dragging() && !mAutoScrolling)
|
|
return;
|
|
|
|
if (event.CmdDown()) {
|
|
// Ctrl-drag has no meaning, fuhggeddaboudit
|
|
return;
|
|
}
|
|
|
|
wxRect r = mCapturedRect;
|
|
Track *pTrack = mCapturedTrack;
|
|
|
|
// AS: Note that FindTrack will replace r's value.
|
|
if (!pTrack)
|
|
pTrack = FindTrack(event.m_x, event.m_y, false, false, &r);
|
|
|
|
// Also fuhggeddaboudit if not in a track.
|
|
if (!pTrack)
|
|
return;
|
|
|
|
int x = mAutoScrolling ? mMouseMostRecentX : event.m_x;
|
|
int y = mAutoScrolling ? mMouseMostRecentY : event.m_y;
|
|
|
|
// JKC: Logic to prevent a selection smaller than 5 pixels to
|
|
// prevent accidental dragging when selecting.
|
|
// (if user really wants a tiny selection, they should zoom in).
|
|
// Can someone make this value of '5' configurable in
|
|
// preferences?
|
|
const int minimumSizedSelection = 5; //measured in pixels
|
|
|
|
// Might be dragging frequency bounds only, test
|
|
if (mSelStartValid) {
|
|
wxInt64 SelStart = mViewInfo->TimeToPosition(mSelStart, r.x); //cvt time to pixels.
|
|
// Abandon this drag if selecting < 5 pixels.
|
|
if (wxLongLong(SelStart-x).Abs() < minimumSizedSelection
|
|
#ifdef USE_MIDI // limiting selection size is good, and not starting
|
|
&& !mStretching // stretch unless mouse moves 5 pixels is good, but
|
|
#endif // once stretching starts, it's ok to move even 1 pixel
|
|
)
|
|
return;
|
|
}
|
|
|
|
// Handle which tracks are selected
|
|
Track *sTrack = pTrack;
|
|
if (Track *eTrack = FindTrack(x, y, false, false, NULL)) {
|
|
// Swap the track pointers if needed
|
|
if (eTrack->GetIndex() < sTrack->GetIndex()) {
|
|
Track *t = eTrack;
|
|
eTrack = sTrack;
|
|
sTrack = t;
|
|
}
|
|
|
|
TrackListIterator iter(mTracks);
|
|
sTrack = iter.StartWith(sTrack);
|
|
do {
|
|
mTracks->Select(sTrack);
|
|
if (sTrack == eTrack) {
|
|
break;
|
|
}
|
|
|
|
sTrack = iter.Next();
|
|
} while (sTrack);
|
|
}
|
|
#ifdef USE_MIDI
|
|
if (mStretching) {
|
|
// the following is also in ExtendSelection, called below
|
|
// probably a good idea to "hoist" the code to before this "if" stmt
|
|
if (clickedTrack == NULL && mCapturedTrack != NULL)
|
|
clickedTrack = mCapturedTrack;
|
|
Stretch(x, r.x, clickedTrack);
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
#ifdef EXPERIMENTAL_SPECTRAL_EDITING
|
|
#ifndef SPECTRAL_EDITING_ESC_KEY
|
|
if (mFreqSelMode == FREQ_SEL_SNAPPING_CENTER &&
|
|
!mViewInfo->selectedRegion.isPoint())
|
|
MoveSnappingFreqSelection(y, r.y, r.height, pTrack);
|
|
else
|
|
#endif
|
|
if (mFreqSelTrack == pTrack)
|
|
ExtendFreqSelection(y, r.y, r.height);
|
|
#endif
|
|
|
|
ExtendSelection(x, r.x, clickedTrack);
|
|
UpdateSelectionDisplay();
|
|
}
|
|
|
|
#ifdef EXPERIMENTAL_SPECTRAL_EDITING
|
|
// Seems 4 is too small to work at the top. Why?
|
|
enum { FREQ_SNAP_DISTANCE = 10 };
|
|
|
|
/// Converts a position (mouse Y coordinate) to
|
|
/// frequency, in Hz.
|
|
double TrackPanel::PositionToFrequency(const WaveTrack *wt,
|
|
bool maySnap,
|
|
wxInt64 mouseYCoordinate,
|
|
wxInt64 trackTopEdge,
|
|
int trackHeight) const
|
|
{
|
|
const double rate = wt->GetRate();
|
|
|
|
// Handle snapping
|
|
if (maySnap &&
|
|
mouseYCoordinate - trackTopEdge < FREQ_SNAP_DISTANCE)
|
|
return rate;
|
|
if (maySnap &&
|
|
trackTopEdge + trackHeight - mouseYCoordinate < FREQ_SNAP_DISTANCE)
|
|
return -1;
|
|
|
|
const SpectrogramSettings &settings = wt->GetSpectrogramSettings();
|
|
const NumberScale numberScale(settings.GetScale(rate, false));
|
|
const double p = double(mouseYCoordinate - trackTopEdge) / trackHeight;
|
|
return numberScale.PositionToValue(1.0 - p);
|
|
}
|
|
|
|
/// Converts a frequency to screen y position.
|
|
wxInt64 TrackPanel::FrequencyToPosition(const WaveTrack *wt,
|
|
double frequency,
|
|
wxInt64 trackTopEdge,
|
|
int trackHeight) const
|
|
{
|
|
const double rate = wt->GetRate();
|
|
|
|
const SpectrogramSettings &settings = wt->GetSpectrogramSettings();
|
|
const NumberScale numberScale(settings.GetScale(rate, false));
|
|
const float p = numberScale.ValueToPosition(frequency);
|
|
return trackTopEdge + wxInt64((1.0 - p) * trackHeight);
|
|
}
|
|
#endif
|
|
|
|
template<typename T>
|
|
inline void SetIfNotNull( T * pValue, const T Value )
|
|
{
|
|
if( pValue == NULL )
|
|
return;
|
|
*pValue = Value;
|
|
}
|
|
|
|
|
|
TrackPanel::SelectionBoundary TrackPanel::ChooseTimeBoundary
|
|
(double selend, bool onlyWithinSnapDistance,
|
|
wxInt64 *pPixelDist, double *pPinValue) const
|
|
{
|
|
const double t0 = mViewInfo->selectedRegion.t0();
|
|
const double t1 = mViewInfo->selectedRegion.t1();
|
|
const wxInt64 posS = mViewInfo->TimeToPosition(selend);
|
|
const wxInt64 pos0 = mViewInfo->TimeToPosition(t0);
|
|
wxInt64 pixelDist = abs(posS - pos0);
|
|
bool chooseLeft = true;
|
|
|
|
if (mViewInfo->selectedRegion.isPoint())
|
|
// Special case when selection is a point, and thus left
|
|
// and right distances are the same
|
|
chooseLeft = (selend < t0);
|
|
else {
|
|
const wxInt64 pos1 = mViewInfo->TimeToPosition(t1);
|
|
const wxInt64 rightDist = abs(posS - pos1);
|
|
if (rightDist < pixelDist)
|
|
chooseLeft = false, pixelDist = rightDist;
|
|
}
|
|
|
|
SetIfNotNull(pPixelDist, pixelDist);
|
|
|
|
if (onlyWithinSnapDistance &&
|
|
pixelDist >= SELECTION_RESIZE_REGION) {
|
|
SetIfNotNull( pPinValue, -1.0);
|
|
return SBNone;
|
|
}
|
|
else if (chooseLeft) {
|
|
SetIfNotNull( pPinValue, t1);
|
|
return SBLeft;
|
|
}
|
|
else {
|
|
SetIfNotNull( pPinValue, t0);
|
|
return SBRight;
|
|
}
|
|
}
|
|
|
|
|
|
TrackPanel::SelectionBoundary TrackPanel::ChooseBoundary
|
|
(wxMouseEvent & event, const Track *pTrack, const wxRect &rect,
|
|
bool mayDragWidth, bool onlyWithinSnapDistance,
|
|
double *pPinValue) const
|
|
{
|
|
// Choose one of four boundaries to adjust, or the center frequency.
|
|
// May choose frequencies only if in a spectrogram view and
|
|
// within the time boundaries.
|
|
// May choose no boundary if onlyWithinSnapDistance is true.
|
|
// Otherwise choose the eligible boundary nearest the mouse click.
|
|
const double selend = mViewInfo->PositionToTime(event.m_x, rect.x);
|
|
wxInt64 pixelDist = 0;
|
|
SelectionBoundary boundary =
|
|
ChooseTimeBoundary(selend, onlyWithinSnapDistance,
|
|
&pixelDist, pPinValue);
|
|
|
|
#ifdef EXPERIMENTAL_SPECTRAL_EDITING
|
|
const double t0 = mViewInfo->selectedRegion.t0();
|
|
const double t1 = mViewInfo->selectedRegion.t1();
|
|
const double f0 = mViewInfo->selectedRegion.f0();
|
|
const double f1 = mViewInfo->selectedRegion.f1();
|
|
const double fc = mViewInfo->selectedRegion.fc();
|
|
double ratio = 0;
|
|
|
|
bool chooseTime = true;
|
|
bool chooseBottom = true;
|
|
bool chooseCenter = false;
|
|
// Consider adjustment of frequencies only if mouse is
|
|
// within the time boundaries
|
|
if (!mViewInfo->selectedRegion.isPoint() &&
|
|
t0 <= selend && selend < t1 &&
|
|
isSpectralSelectionTrack(pTrack)) {
|
|
const WaveTrack *const wt = static_cast<const WaveTrack*>(pTrack);
|
|
const wxInt64 bottomSel = (f0 >= 0)
|
|
? FrequencyToPosition(wt, f0, rect.y, rect.height)
|
|
: rect.y + rect.height;
|
|
const wxInt64 topSel = (f1 >= 0)
|
|
? FrequencyToPosition(wt, f1, rect.y, rect.height)
|
|
: rect.y;
|
|
wxInt64 signedBottomDist = int(event.m_y - bottomSel);
|
|
wxInt64 verticalDist = abs(signedBottomDist);
|
|
if (bottomSel == topSel)
|
|
// Top and bottom are too close to resolve on screen
|
|
chooseBottom = (signedBottomDist >= 0);
|
|
else {
|
|
const wxInt64 topDist = abs(int(event.m_y - topSel));
|
|
if (topDist < verticalDist)
|
|
chooseBottom = false, verticalDist = topDist;
|
|
}
|
|
if (fc > 0
|
|
#ifdef SPECTRAL_EDITING_ESC_KEY
|
|
&& mayDragWidth
|
|
#endif
|
|
) {
|
|
const wxInt64 centerSel =
|
|
FrequencyToPosition(wt, fc, rect.y, rect.height);
|
|
const wxInt64 centerDist = abs(int(event.m_y - centerSel));
|
|
if (centerDist < verticalDist)
|
|
chooseCenter = true, verticalDist = centerDist,
|
|
ratio = f1 / fc;
|
|
}
|
|
if (verticalDist >= 0 &&
|
|
verticalDist < pixelDist) {
|
|
pixelDist = verticalDist;
|
|
chooseTime = false;
|
|
}
|
|
}
|
|
|
|
if (!chooseTime) {
|
|
// PRL: Seems I need a larger tolerance to make snapping work
|
|
// at top of track, not sure why
|
|
if (onlyWithinSnapDistance &&
|
|
pixelDist >= FREQ_SNAP_DISTANCE) {
|
|
SetIfNotNull( pPinValue, -1.0);
|
|
return SBNone;
|
|
}
|
|
else if (chooseCenter) {
|
|
SetIfNotNull( pPinValue, ratio);
|
|
return SBCenter;
|
|
}
|
|
else if (mayDragWidth && fc > 0) {
|
|
SetIfNotNull(pPinValue, fc);
|
|
return SBWidth;
|
|
}
|
|
else if (chooseBottom) {
|
|
SetIfNotNull( pPinValue, f1 );
|
|
return SBBottom;
|
|
}
|
|
else {
|
|
SetIfNotNull(pPinValue, f0);
|
|
return SBTop;
|
|
}
|
|
}
|
|
else
|
|
#endif
|
|
{
|
|
return boundary;
|
|
}
|
|
}
|
|
|
|
/// HandleEnvelope gets called when the user is changing the
|
|
/// amplitude envelope on a track.
|
|
void TrackPanel::HandleEnvelope(wxMouseEvent & event)
|
|
{
|
|
if (event.LeftDown()) {
|
|
wxRect r;
|
|
Track *pTrack = FindTrack(event.m_x, event.m_y, false, false, &r);
|
|
|
|
if (!pTrack)
|
|
return;
|
|
|
|
SetCapturedTrack(pTrack, IsEnveloping);
|
|
|
|
if (mCapturedTrack->GetKind() == Track::Wave)
|
|
{
|
|
mCapturedEnvelope =
|
|
((WaveTrack*)mCapturedTrack)->GetEnvelopeAtX(event.GetX());
|
|
} else {
|
|
mCapturedEnvelope = NULL;
|
|
}
|
|
|
|
mCapturedRect = r;
|
|
mCapturedRect.y += kTopInset;
|
|
mCapturedRect.height -= kTopInset;
|
|
}
|
|
// AS: if there's actually a selected track, then forward all of the
|
|
// mouse events to its envelope.
|
|
if (mCapturedTrack)
|
|
ForwardEventToEnvelope(event);
|
|
|
|
if (event.LeftUp()) {
|
|
mCapturedTrack = NULL;
|
|
MakeParentPushState(
|
|
/* i18n-hint: (verb) Audacity has just adjusted the envelope .*/
|
|
_("Adjusted envelope."),
|
|
/* i18n-hint: The envelope is a curve that controls the audio loudness.*/
|
|
_("Envelope")
|
|
);
|
|
}
|
|
}
|
|
|
|
/// We've established we're a time track.
|
|
/// send events for its envelope.
|
|
void TrackPanel::ForwardEventToTimeTrackEnvelope(wxMouseEvent & event)
|
|
{
|
|
TimeTrack *ptimetrack = (TimeTrack *) mCapturedTrack;
|
|
Envelope *pspeedenvelope = ptimetrack->GetEnvelope();
|
|
|
|
wxRect envRect = mCapturedRect;
|
|
envRect.y++;
|
|
envRect.height -= 2;
|
|
double lower = ptimetrack->GetRangeLower(), upper = ptimetrack->GetRangeUpper();
|
|
if(ptimetrack->GetDisplayLog()) {
|
|
// MB: silly way to undo the work of GetWaveYPos while still getting a logarithmic scale
|
|
double dBRange = mViewInfo->dBr;
|
|
lower = LINEAR_TO_DB(std::max(1.0e-7, lower)) / dBRange + 1.0;
|
|
upper = LINEAR_TO_DB(std::max(1.0e-7, upper)) / dBRange + 1.0;
|
|
}
|
|
bool needUpdate =
|
|
pspeedenvelope->MouseEvent(
|
|
event, envRect,
|
|
*mViewInfo,
|
|
ptimetrack->GetDisplayLog(), lower, upper);
|
|
if (needUpdate) {
|
|
RefreshTrack(mCapturedTrack);
|
|
}
|
|
}
|
|
|
|
/// We've established we're a wave track.
|
|
/// send events for its envelope.
|
|
void TrackPanel::ForwardEventToWaveTrackEnvelope(wxMouseEvent & event)
|
|
{
|
|
WaveTrack *pwavetrack = (WaveTrack *) mCapturedTrack;
|
|
Envelope *penvelope = mCapturedEnvelope;
|
|
|
|
// Possibly no-envelope, for example when in spectrum view mode.
|
|
// if so, then bail out.
|
|
if (!penvelope)
|
|
return;
|
|
|
|
// AS: WaveTracks can be displayed in several different formats.
|
|
// This asks which one is in use. (ie, Wave, Spectrum, etc)
|
|
int display = pwavetrack->GetDisplay();
|
|
|
|
if (display == WaveTrack::Waveform) {
|
|
const bool dB = !pwavetrack->GetWaveformSettings().isLinear();
|
|
bool needUpdate;
|
|
|
|
// AS: Then forward our mouse event to the envelope.
|
|
// It'll recalculate and then tell us whether or not to redraw.
|
|
wxRect envRect = mCapturedRect;
|
|
envRect.y++;
|
|
envRect.height -= 2;
|
|
float zoomMin, zoomMax;
|
|
pwavetrack->GetDisplayBounds(&zoomMin, &zoomMax);
|
|
needUpdate = penvelope->MouseEvent(
|
|
event, envRect,
|
|
*mViewInfo,
|
|
dB, zoomMin, zoomMax);
|
|
|
|
// If this track is linked to another track, make the identical
|
|
// change to the linked envelope:
|
|
WaveTrack *link = (WaveTrack *) mCapturedTrack->GetLink();
|
|
if (link) {
|
|
Envelope *e2 = link->GetEnvelopeAtX(event.GetX());
|
|
// There isn't necessarily an envelope there; no guarantee a
|
|
// linked track has the same WaveClip structure...
|
|
bool updateNeeded = false;
|
|
if (e2) {
|
|
wxRect envRect = mCapturedRect;
|
|
envRect.y++;
|
|
envRect.height -= 2;
|
|
float zoomMin, zoomMax;
|
|
pwavetrack->GetDisplayBounds(&zoomMin, &zoomMax);
|
|
updateNeeded = e2->MouseEvent(event, envRect,
|
|
*mViewInfo, dB,
|
|
zoomMin, zoomMax);
|
|
needUpdate |= updateNeeded;
|
|
}
|
|
if(!e2 || !updateNeeded) // no envelope found at this x point, or found but not updated
|
|
{
|
|
if( (e2 = link->GetActiveEnvelope()) != 0 ) // search for any active DragPoint
|
|
{
|
|
wxRect envRect = mCapturedRect;
|
|
envRect.y++;
|
|
envRect.height -= 2;
|
|
float zoomMin, zoomMax;
|
|
pwavetrack->GetDisplayBounds(&zoomMin, &zoomMax);
|
|
needUpdate |= e2->MouseEvent(event, envRect,
|
|
*mViewInfo, dB,
|
|
zoomMin, zoomMax);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (needUpdate) {
|
|
RefreshTrack(mCapturedTrack);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/// The Envelope class actually handles things at the mouse
|
|
/// event level, so we have to forward the events over. Envelope
|
|
/// will then tell us whether or not we need to redraw.
|
|
|
|
// AS: I'm not sure why we can't let the Envelope take care of
|
|
// redrawing itself. ?
|
|
|
|
void TrackPanel::ForwardEventToEnvelope(wxMouseEvent & event)
|
|
{
|
|
if (mCapturedTrack && mCapturedTrack->GetKind() == Track::Time)
|
|
{
|
|
ForwardEventToTimeTrackEnvelope( event );
|
|
}
|
|
else if (mCapturedTrack && mCapturedTrack->GetKind() == Track::Wave)
|
|
{
|
|
ForwardEventToWaveTrackEnvelope( event );
|
|
}
|
|
}
|
|
|
|
void TrackPanel::HandleSlide(wxMouseEvent & event)
|
|
{
|
|
if (event.LeftDown())
|
|
StartSlide(event);
|
|
|
|
if (mMouseCapture != IsSliding)
|
|
return;
|
|
|
|
if (event.Dragging() && mCapturedTrack)
|
|
DoSlide(event);
|
|
|
|
if (event.LeftUp()) {
|
|
if (mDidSlideVertically && mCapturedTrack)
|
|
// Now that user has dropped the clip into a different track,
|
|
// make sure the sample rate matches the destination track (mCapturedTrack).
|
|
for (size_t i = 0; i < mCapturedClipArray.size(); i++)
|
|
if (mCapturedTrack->GetKind() == Track::Wave) // Should always be true here, but make sure.
|
|
{
|
|
WaveClip* pWaveClip = mCapturedClipArray[i].clip;
|
|
// Note that per TrackPanel::AddClipsToCaptured(Track *t, double t0, double t1),
|
|
// in the non-WaveTrack case, the code adds a NULL clip to mCapturedClipArray,
|
|
// so we have to check for that any time we're going to deref it.
|
|
// Previous code did not check it here, and that caused bug 367 crash.
|
|
if (pWaveClip)
|
|
{
|
|
pWaveClip->Resample(((WaveTrack*)mCapturedTrack)->GetRate());
|
|
pWaveClip->MarkChanged();
|
|
}
|
|
}
|
|
|
|
SetCapturedTrack( NULL );
|
|
|
|
if (mSnapManager) {
|
|
delete mSnapManager;
|
|
mSnapManager = NULL;
|
|
}
|
|
mSnapLeft = -1;
|
|
mSnapRight = -1;
|
|
|
|
if (!mDidSlideVertically && mHSlideAmount==0)
|
|
return;
|
|
|
|
MakeParentRedrawScrollbars();
|
|
|
|
wxString msg;
|
|
bool consolidate;
|
|
if (mDidSlideVertically) {
|
|
msg.Printf(_("Moved clip to another track"));
|
|
consolidate = false;
|
|
}
|
|
else {
|
|
wxString direction = mHSlideAmount>0 ?
|
|
/* i18n-hint: a direction as in left or right.*/
|
|
_("right") :
|
|
/* i18n-hint: a direction as in left or right.*/
|
|
_("left");
|
|
/* i18n-hint: %s is a direction like left or right */
|
|
msg.Printf(_("Time shifted tracks/clips %s %.02f seconds"),
|
|
direction.c_str(), fabs(mHSlideAmount));
|
|
consolidate = true;
|
|
}
|
|
MakeParentPushState(msg, _("Time-Shift"),
|
|
consolidate ? (PUSH_CONSOLIDATE) : (PUSH_AUTOSAVE));
|
|
}
|
|
}
|
|
|
|
/// Prepare for sliding.
|
|
void TrackPanel::StartSlide(wxMouseEvent & event)
|
|
{
|
|
wxRect r;
|
|
|
|
mHSlideAmount = 0.0;
|
|
mDidSlideVertically = false;
|
|
|
|
Track *vt = FindTrack(event.m_x, event.m_y, false, false, &r);
|
|
if (!vt)
|
|
return;
|
|
|
|
ToolsToolBar * ttb = mListener->TP_GetToolsToolBar();
|
|
bool multiToolModeActive = (ttb && ttb->IsDown(multiTool));
|
|
|
|
if ((vt->GetKind() == Track::Wave
|
|
#ifdef USE_MIDI
|
|
|| vt->GetKind() == Track::Note
|
|
#endif
|
|
) && !event.ShiftDown())
|
|
{
|
|
#ifdef USE_MIDI
|
|
if (vt->GetKind() == Track::Wave) {
|
|
#endif
|
|
WaveTrack* wt = (WaveTrack*)vt;
|
|
mCapturedClip = wt->GetClipAtX(event.m_x);
|
|
if (mCapturedClip == NULL)
|
|
return;
|
|
#ifdef USE_MIDI
|
|
}
|
|
else {
|
|
mCapturedClip = NULL;
|
|
}
|
|
#endif
|
|
// The captured clip is the focus, but we need to create a list
|
|
// of all clips that have to move, also...
|
|
|
|
mCapturedClipArray.clear();
|
|
|
|
double clickTime =
|
|
mViewInfo->PositionToTime(event.m_x, GetLeftOffset());
|
|
bool clickedInSelection =
|
|
(vt->GetSelected() &&
|
|
clickTime > mViewInfo->selectedRegion.t0() &&
|
|
clickTime < mViewInfo->selectedRegion.t1());
|
|
|
|
// First, if click was in selection, capture selected clips; otherwise
|
|
// just the clicked-on clip
|
|
if (clickedInSelection) {
|
|
mCapturedClipIsSelection = true;
|
|
|
|
TrackListIterator iter(mTracks);
|
|
for (Track *t = iter.First(); t; t = iter.Next()) {
|
|
if (t->GetSelected()) {
|
|
AddClipsToCaptured(t, true);
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
mCapturedClipIsSelection = false;
|
|
mCapturedClipArray.push_back(TrackClip(vt, mCapturedClip));
|
|
|
|
// Check for stereo partner
|
|
Track *partner = mTracks->GetLink(vt);
|
|
if (mCapturedClip && partner && partner->GetKind() == Track::Wave) {
|
|
// WaveClip::GetClipAtX doesn't work unless the clip is on the screen and can return bad info otherwise
|
|
// instead calculate the time manually
|
|
double rate = ((WaveTrack*)partner)->GetRate();
|
|
const double tt = mViewInfo->PositionToTime(event.m_x, GetLeftOffset());
|
|
sampleCount s0 = (sampleCount)(tt * rate + 0.5);
|
|
|
|
if (s0 >= 0) {
|
|
WaveClip *clip = ((WaveTrack *)partner)->GetClipAtSample(s0);
|
|
if (clip) {
|
|
mCapturedClipArray.push_back(TrackClip(partner, clip));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Now, if sync-lock is enabled, capture any clip that's linked to a
|
|
// captured clip.
|
|
if (GetProject()->IsSyncLocked()) {
|
|
// AWD: mCapturedClipArray expands as the loop runs, so newly-added
|
|
// clips are considered (the effect is like recursion and terminates
|
|
// because AddClipsToCaptured doesn't add duplicate clips); to remove
|
|
// this behavior just store the array size beforehand.
|
|
for (unsigned int i = 0; i < mCapturedClipArray.size(); ++i) {
|
|
// Capture based on tracks that have clips -- that means we
|
|
// don't capture based on links to label tracks for now (until
|
|
// we can treat individual labels as clips)
|
|
if (mCapturedClipArray[i].clip) {
|
|
// Iterate over sync-lock group tracks.
|
|
SyncLockedTracksIterator git(mTracks);
|
|
for ( Track *t = git.First(mCapturedClipArray[i].track);
|
|
t; t = git.Next() )
|
|
{
|
|
AddClipsToCaptured(t,
|
|
mCapturedClipArray[i].clip->GetStartTime(),
|
|
mCapturedClipArray[i].clip->GetEndTime() );
|
|
}
|
|
}
|
|
#ifdef USE_MIDI
|
|
// Capture additional clips from NoteTracks
|
|
Track *nt = mCapturedClipArray[i].track;
|
|
if (nt->GetKind() == Track::Note) {
|
|
// Iterate over sync-lock group tracks.
|
|
SyncLockedTracksIterator git(mTracks);
|
|
for (Track *t = git.First(nt); t; t = git.Next())
|
|
{
|
|
AddClipsToCaptured(t, nt->GetStartTime(), nt->GetEndTime());
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
} else {
|
|
mCapturedClip = NULL;
|
|
mCapturedClipArray.clear();
|
|
}
|
|
|
|
mSlideUpDownOnly = event.CmdDown() && !multiToolModeActive;
|
|
|
|
mCapturedTrack = vt;
|
|
mCapturedRect = r;
|
|
|
|
mMouseClickX = event.m_x;
|
|
mMouseClickY = event.m_y;
|
|
|
|
mSelStartValid = true;
|
|
mSelStart = mViewInfo->PositionToTime(event.m_x, r.x);
|
|
|
|
if (mSnapManager)
|
|
delete mSnapManager;
|
|
mSnapManager = new SnapManager(mTracks,
|
|
&mCapturedClipArray,
|
|
*mViewInfo,
|
|
4, // pixel tolerance
|
|
true); // don't snap to time
|
|
mSnapLeft = -1;
|
|
mSnapRight = -1;
|
|
mSnapPreferRightEdge = false;
|
|
if (mCapturedClip) {
|
|
if (fabs(mSelStart - mCapturedClip->GetEndTime()) <
|
|
fabs(mSelStart - mCapturedClip->GetStartTime()))
|
|
mSnapPreferRightEdge = true;
|
|
}
|
|
|
|
mMouseCapture = IsSliding;
|
|
}
|
|
|
|
// Helper for the above, adds a track's clips to mCapturedClipArray (eliminates
|
|
// duplication of this logic)
|
|
void TrackPanel::AddClipsToCaptured(Track *t, bool withinSelection)
|
|
{
|
|
if (withinSelection)
|
|
AddClipsToCaptured(t, mViewInfo->selectedRegion.t0(),
|
|
mViewInfo->selectedRegion.t1());
|
|
else
|
|
AddClipsToCaptured(t, t->GetStartTime(), t->GetEndTime());
|
|
}
|
|
|
|
// Adds a track's clips to mCapturedClipArray within a specified time
|
|
void TrackPanel::AddClipsToCaptured(Track *t, double t0, double t1)
|
|
{
|
|
if (t->GetKind() == Track::Wave)
|
|
{
|
|
WaveClipList::compatibility_iterator it =
|
|
((WaveTrack *)t)->GetClipIterator();
|
|
while (it)
|
|
{
|
|
WaveClip *clip = it->GetData();
|
|
|
|
if ( ! clip->AfterClip(t0) && ! clip->BeforeClip(t1) )
|
|
{
|
|
// Avoid getting clips that were already captured
|
|
bool newClip = true;
|
|
for (unsigned int i = 0; i < mCapturedClipArray.size(); ++i) {
|
|
if (mCapturedClipArray[i].clip == clip) {
|
|
newClip = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (newClip)
|
|
mCapturedClipArray.push_back(TrackClip(t, clip));
|
|
}
|
|
it = it->GetNext();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// This handles label tracks rather heavy-handedly -- it would be nice to
|
|
// treat individual labels like clips
|
|
|
|
// Avoid adding a track twice
|
|
bool newClip = true;
|
|
for (unsigned int i = 0; i < mCapturedClipArray.size(); ++i) {
|
|
if (mCapturedClipArray[i].track == t) {
|
|
newClip = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (newClip) {
|
|
#ifdef USE_MIDI
|
|
// do not add NoteTrack if the data is outside of time bounds
|
|
if (t->GetKind() == Track::Note) {
|
|
if (t->GetEndTime() < t0 || t->GetStartTime() > t1)
|
|
return;
|
|
}
|
|
#endif
|
|
mCapturedClipArray.push_back(TrackClip(t, NULL));
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Slide tracks horizontally, or slide clips horizontally or vertically
|
|
/// (e.g. moving clips between tracks).
|
|
|
|
// GM: DoSlide now implementing snap-to
|
|
// samples functionality based on sample rate.
|
|
void TrackPanel::DoSlide(wxMouseEvent & event)
|
|
{
|
|
unsigned int i;
|
|
|
|
// find which track the mouse is currently in (mouseTrack) -
|
|
// this may not be the same as the one we started in...
|
|
#ifdef USE_MIDI
|
|
Track *mouseTrack = FindTrack(event.m_x, event.m_y, false, false, NULL);
|
|
if (!mouseTrack || (mouseTrack->GetKind() != Track::Wave &&
|
|
mouseTrack->GetKind() != Track::Note)) {
|
|
#else
|
|
WaveTrack *mouseTrack =
|
|
(WaveTrack *)FindTrack(event.m_x, event.m_y, false, false, NULL);
|
|
if (!mouseTrack || mouseTrack->GetKind() != Track::Wave) {
|
|
#endif
|
|
return;
|
|
}
|
|
|
|
// Start by undoing the current slide amount; everything
|
|
// happens relative to the original horizontal position of
|
|
// each clip...
|
|
#ifdef USE_MIDI
|
|
if (mCapturedClipArray.size()) {
|
|
#else
|
|
if (mCapturedClip) {
|
|
#endif
|
|
for(i=0; i<mCapturedClipArray.size(); i++) {
|
|
if (mCapturedClipArray[i].clip)
|
|
mCapturedClipArray[i].clip->Offset(-mHSlideAmount);
|
|
else
|
|
mCapturedClipArray[i].track->Offset(-mHSlideAmount);
|
|
}
|
|
}
|
|
else {
|
|
mCapturedTrack->Offset(-mHSlideAmount);
|
|
Track* link = mTracks->GetLink(mCapturedTrack);
|
|
if (link)
|
|
link->Offset(-mHSlideAmount);
|
|
}
|
|
|
|
if (mCapturedClipIsSelection) {
|
|
// Slide the selection, too
|
|
mViewInfo->selectedRegion.move(-mHSlideAmount);
|
|
}
|
|
mHSlideAmount = 0.0;
|
|
|
|
double desiredSlideAmount =
|
|
mViewInfo->PositionToTime(event.m_x) -
|
|
mViewInfo->PositionToTime(mMouseClickX);
|
|
#ifdef USE_MIDI
|
|
if (mouseTrack->GetKind() == Track::Wave) {
|
|
WaveTrack *mtw = (WaveTrack *) mouseTrack;
|
|
desiredSlideAmount = rint(mtw->GetRate() * desiredSlideAmount) /
|
|
mtw->GetRate(); // set it to a sample point
|
|
}
|
|
// Adjust desiredSlideAmount using SnapManager
|
|
if (mSnapManager && mCapturedClipArray.size()) {
|
|
double clipLeft;
|
|
double clipRight;
|
|
if (mCapturedClip) {
|
|
clipLeft = mCapturedClip->GetStartTime() + desiredSlideAmount;
|
|
clipRight = mCapturedClip->GetEndTime() + desiredSlideAmount;
|
|
}
|
|
else {
|
|
clipLeft = mCapturedTrack->GetStartTime() + desiredSlideAmount;
|
|
clipRight = mCapturedTrack->GetEndTime() + desiredSlideAmount;
|
|
}
|
|
#else
|
|
desiredSlideAmount = rint(mouseTrack->GetRate() * desiredSlideAmount) /
|
|
mouseTrack->GetRate(); // set it to a sample point
|
|
if (mSnapManager && mCapturedClip) {
|
|
double clipLeft = mCapturedClip->GetStartTime() + desiredSlideAmount;
|
|
double clipRight = mCapturedClip->GetEndTime() + desiredSlideAmount;
|
|
#endif
|
|
|
|
double newClipLeft = clipLeft;
|
|
double newClipRight = clipRight;
|
|
|
|
bool dummy1, dummy2;
|
|
mSnapManager->Snap(mCapturedTrack, clipLeft, false, &newClipLeft,
|
|
&dummy1, &dummy2);
|
|
mSnapManager->Snap(mCapturedTrack, clipRight, false, &newClipRight,
|
|
&dummy1, &dummy2);
|
|
|
|
// Only one of them is allowed to snap
|
|
if (newClipLeft != clipLeft && newClipRight != clipRight) {
|
|
if (mSnapPreferRightEdge)
|
|
newClipLeft = clipLeft;
|
|
else
|
|
newClipRight = clipRight;
|
|
}
|
|
|
|
// Take whichever one snapped (if any) and compute the new desiredSlideAmount
|
|
mSnapLeft = -1;
|
|
mSnapRight = -1;
|
|
if (newClipLeft != clipLeft) {
|
|
double difference = (newClipLeft - clipLeft);
|
|
desiredSlideAmount += difference;
|
|
mSnapLeft = mViewInfo->TimeToPosition(newClipLeft, GetLeftOffset());
|
|
}
|
|
else if (newClipRight != clipRight) {
|
|
double difference = (newClipRight - clipRight);
|
|
desiredSlideAmount += difference;
|
|
mSnapRight = mViewInfo->TimeToPosition(newClipRight, GetLeftOffset());
|
|
}
|
|
}
|
|
|
|
// Implement sliding within the track(s)
|
|
if (mSlideUpDownOnly) {
|
|
desiredSlideAmount = 0.0;
|
|
}
|
|
|
|
// Scroll during vertical drag.
|
|
// EnsureVisible(mouseTrack); //vvv Gale says this has problems on Linux, per bug 393 thread. Revert for 2.0.2.
|
|
|
|
//If the mouse is over a track that isn't the captured track,
|
|
//drag the clip to the mousetrack
|
|
if (mCapturedClip && mouseTrack != mCapturedTrack /*&&
|
|
!mCapturedClipIsSelection*/)
|
|
{
|
|
// Make sure we always have the first linked track of a stereo track
|
|
if (!mouseTrack->GetLinked() && mTracks->GetLink(mouseTrack))
|
|
mouseTrack =
|
|
#ifndef USE_MIDI
|
|
(WaveTrack *)
|
|
#endif
|
|
mTracks->GetLink(mouseTrack);
|
|
|
|
// Temporary apply the offset because we want to see if the
|
|
// track fits with the desired offset
|
|
for(i=0; i<mCapturedClipArray.size(); i++)
|
|
if (mCapturedClipArray[i].clip)
|
|
mCapturedClipArray[i].clip->Offset(desiredSlideAmount);
|
|
// See if it can be moved
|
|
if (MoveClipToTrack(mCapturedClip,
|
|
(WaveTrack*)mouseTrack)) {
|
|
mCapturedTrack = mouseTrack;
|
|
mDidSlideVertically = true;
|
|
|
|
if (mCapturedClipIsSelection) {
|
|
// Slide the selection, too
|
|
mViewInfo->selectedRegion.move(desiredSlideAmount);
|
|
}
|
|
|
|
// Make the offset permanent; start from a "clean slate"
|
|
mHSlideAmount = 0.0;
|
|
desiredSlideAmount = 0.0;
|
|
mMouseClickX = event.m_x;
|
|
}
|
|
else {
|
|
// Undo the offset
|
|
for(i=0; i<mCapturedClipArray.size(); i++)
|
|
if (mCapturedClipArray[i].clip)
|
|
mCapturedClipArray[i].clip->Offset(-desiredSlideAmount);
|
|
}
|
|
|
|
Refresh(false);
|
|
}
|
|
|
|
// Implement sliding within the track(s)
|
|
if (mSlideUpDownOnly)
|
|
return;
|
|
|
|
// Determine desired amount to slide
|
|
mHSlideAmount = desiredSlideAmount;
|
|
|
|
if (mHSlideAmount == 0.0) {
|
|
Refresh(false);
|
|
return;
|
|
}
|
|
|
|
#ifdef USE_MIDI
|
|
if (mCapturedClipArray.size()) {
|
|
#else
|
|
if (mCapturedClip) {
|
|
#endif
|
|
double allowed;
|
|
double initialAllowed;
|
|
double safeBigDistance = 1000 + 2.0 * (mTracks->GetEndTime() -
|
|
mTracks->GetStartTime());
|
|
|
|
do { // loop to compute allowed, does not actually move anything yet
|
|
initialAllowed = mHSlideAmount;
|
|
|
|
unsigned int i, j;
|
|
for(i=0; i<mCapturedClipArray.size(); i++) {
|
|
WaveTrack *track = (WaveTrack *)mCapturedClipArray[i].track;
|
|
WaveClip *clip = mCapturedClipArray[i].clip;
|
|
|
|
if (clip) { // only audio clips are used to compute allowed
|
|
// Move all other selected clips totally out of the way
|
|
// temporarily because they're all moving together and
|
|
// we want to find out if OTHER clips are in the way,
|
|
// not one of the moving ones
|
|
for(j=0; j<mCapturedClipArray.size(); j++) {
|
|
WaveClip *clip2 = mCapturedClipArray[j].clip;
|
|
if (clip2 && clip2 != clip)
|
|
clip2->Offset(-safeBigDistance);
|
|
}
|
|
|
|
if (track->CanOffsetClip(clip, mHSlideAmount, &allowed)) {
|
|
mHSlideAmount = allowed;
|
|
}
|
|
else
|
|
mHSlideAmount = 0.0;
|
|
|
|
for(j=0; j<mCapturedClipArray.size(); j++) {
|
|
WaveClip *clip2 = mCapturedClipArray[j].clip;
|
|
if (clip2 && clip2 != clip)
|
|
clip2->Offset(safeBigDistance);
|
|
}
|
|
}
|
|
}
|
|
} while (mHSlideAmount != initialAllowed);
|
|
|
|
if (mHSlideAmount != 0.0) { // finally, here is where clips are moved
|
|
unsigned int i;
|
|
for(i=0; i<mCapturedClipArray.size(); i++) {
|
|
Track *track = mCapturedClipArray[i].track;
|
|
WaveClip *clip = mCapturedClipArray[i].clip;
|
|
if (clip)
|
|
clip->Offset(mHSlideAmount);
|
|
else
|
|
track->Offset(mHSlideAmount);
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
// For non wavetracks, specifically label tracks ...
|
|
mCapturedTrack->Offset(mHSlideAmount);
|
|
Track* link = mTracks->GetLink(mCapturedTrack);
|
|
if (link)
|
|
link->Offset(mHSlideAmount);
|
|
}
|
|
if (mCapturedClipIsSelection) {
|
|
// Slide the selection, too
|
|
mViewInfo->selectedRegion.move(mHSlideAmount);
|
|
}
|
|
|
|
Refresh(false);
|
|
}
|
|
|
|
|
|
/// This method takes care of our different zoom
|
|
/// possibilities. It is possible for a user to just
|
|
/// "zoom in" or "zoom out," but it is also possible
|
|
/// for a user to drag and select an area that he
|
|
/// or she wants to be zoomed in on. We use mZoomStart
|
|
/// and mZoomEnd to track the beggining and end of such
|
|
/// a zoom area. Note that the ViewInfo member
|
|
/// mViewInfo actually keeps track of our zoom constant,
|
|
/// so we achieve zooming by altering the zoom constant
|
|
/// and forcing a refresh.
|
|
void TrackPanel::HandleZoom(wxMouseEvent & event)
|
|
{
|
|
if (event.ButtonDown() || event.LeftDClick()) {
|
|
HandleZoomClick(event);
|
|
}
|
|
else if (mMouseCapture == IsZooming) {
|
|
if (event.Dragging()) {
|
|
HandleZoomDrag(event);
|
|
}
|
|
else if (event.ButtonUp()) {
|
|
HandleZoomButtonUp(event);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Zoom button down, record the position.
|
|
void TrackPanel::HandleZoomClick(wxMouseEvent & event)
|
|
{
|
|
if (mCapturedTrack)
|
|
return;
|
|
|
|
mCapturedTrack = FindTrack(event.m_x, event.m_y, false, false,
|
|
&mCapturedRect);
|
|
if (!mCapturedTrack)
|
|
return;
|
|
|
|
SetCapturedTrack(mCapturedTrack, IsZooming);
|
|
|
|
mZoomStart = event.m_x;
|
|
mZoomEnd = event.m_x;
|
|
}
|
|
|
|
/// Zoom drag
|
|
void TrackPanel::HandleZoomDrag(wxMouseEvent & event)
|
|
{
|
|
int left, width, height;
|
|
|
|
left = GetLeftOffset();
|
|
GetTracksUsableArea(&width, &height);
|
|
|
|
mZoomEnd = event.m_x;
|
|
|
|
if (event.m_x < left) {
|
|
mZoomEnd = left;
|
|
}
|
|
else if (event.m_x >= left + width - 1) {
|
|
mZoomEnd = left + width - 1;
|
|
}
|
|
|
|
if (IsDragZooming()) {
|
|
Refresh(false);
|
|
}
|
|
}
|
|
|
|
/// Zoom button up
|
|
void TrackPanel::HandleZoomButtonUp(wxMouseEvent & event)
|
|
{
|
|
if (mZoomEnd < mZoomStart) {
|
|
int temp = mZoomEnd;
|
|
mZoomEnd = mZoomStart;
|
|
mZoomStart = temp;
|
|
}
|
|
|
|
if (IsDragZooming())
|
|
DragZoom(event, GetLeftOffset());
|
|
else
|
|
DoZoomInOut(event, GetLeftOffset());
|
|
|
|
mZoomEnd = mZoomStart = 0;
|
|
|
|
SetCapturedTrack(NULL);
|
|
|
|
MakeParentRedrawScrollbars();
|
|
Refresh(false);
|
|
}
|
|
|
|
/// Determines if drag zooming is active
|
|
bool TrackPanel::IsDragZooming()
|
|
{
|
|
return (abs(mZoomEnd - mZoomStart) > DragThreshold);
|
|
}
|
|
|
|
/// Determines if the a modal tool is active
|
|
bool TrackPanel::IsMouseCaptured()
|
|
{
|
|
return (mMouseCapture != IsUncaptured || mCapturedTrack != NULL);
|
|
}
|
|
|
|
|
|
/// This actually sets the Zoom value when you're done doing
|
|
/// a drag zoom.
|
|
void TrackPanel::DragZoom(wxMouseEvent & event, int trackLeftEdge)
|
|
{
|
|
double left = mViewInfo->PositionToTime(mZoomStart, trackLeftEdge);
|
|
double right = mViewInfo->PositionToTime(mZoomEnd, trackLeftEdge);
|
|
|
|
double multiplier = mViewInfo->screen / (right - left);
|
|
if (event.ShiftDown())
|
|
multiplier = 1.0 / multiplier;
|
|
|
|
mViewInfo->ZoomBy(multiplier);
|
|
|
|
mViewInfo->h = left;
|
|
}
|
|
|
|
/// This handles normal Zoom In/Out, if you just clicked;
|
|
/// IOW, if you were NOT dragging to zoom an area.
|
|
/// \todo MAGIC NUMBER: We've got several in this method.
|
|
void TrackPanel::DoZoomInOut(wxMouseEvent & event, int trackLeftEdge)
|
|
{
|
|
double center_h = mViewInfo->PositionToTime(event.m_x, trackLeftEdge);
|
|
|
|
const double multiplier =
|
|
(event.RightUp() || event.RightDClick() || event.ShiftDown())
|
|
? 0.5 : 2.0;
|
|
mViewInfo->ZoomBy(multiplier);
|
|
|
|
if (event.MiddleUp() || event.MiddleDClick())
|
|
mViewInfo->SetZoom(ZoomInfo::GetDefaultZoom()); // AS: Reset zoom.
|
|
|
|
double new_center_h = mViewInfo->PositionToTime(event.m_x, trackLeftEdge);
|
|
|
|
mViewInfo->h += (center_h - new_center_h);
|
|
}
|
|
|
|
/// Vertical zooming (triggered by clicking in the
|
|
/// vertical ruler)
|
|
void TrackPanel::HandleVZoom(wxMouseEvent & event)
|
|
{
|
|
if (event.ButtonDown() || event.ButtonDClick()) {
|
|
HandleVZoomClick( event );
|
|
}
|
|
else if (event.Dragging()) {
|
|
HandleVZoomDrag( event );
|
|
}
|
|
else if (event.ButtonUp()) {
|
|
HandleVZoomButtonUp( event );
|
|
}
|
|
//TODO-MB: add timetrack zooming here!
|
|
}
|
|
|
|
/// VZoom click
|
|
void TrackPanel::HandleVZoomClick( wxMouseEvent & event )
|
|
{
|
|
if (mCapturedTrack)
|
|
return;
|
|
mCapturedTrack = FindTrack(event.m_x, event.m_y, true, false,
|
|
&mCapturedRect);
|
|
if (!mCapturedTrack)
|
|
return;
|
|
|
|
if (mCapturedTrack->GetKind() == Track::Wave
|
|
#ifdef USE_MIDI
|
|
|| mCapturedTrack->GetKind() == Track::Note
|
|
#endif
|
|
)
|
|
{
|
|
mMouseCapture = IsVZooming;
|
|
mZoomStart = event.m_y;
|
|
mZoomEnd = event.m_y;
|
|
// change note track to zoom like audio track
|
|
//#ifdef USE_MIDI
|
|
// if (mCapturedTrack->GetKind() == Track::Note) {
|
|
// ((NoteTrack *) mCapturedTrack)->StartVScroll();
|
|
// }
|
|
//#endif
|
|
}
|
|
}
|
|
|
|
/// VZoom drag
|
|
void TrackPanel::HandleVZoomDrag( wxMouseEvent & event )
|
|
{
|
|
mZoomEnd = event.m_y;
|
|
if (IsDragZooming()){
|
|
// changed Note track to work like audio track
|
|
//#ifdef USE_MIDI
|
|
// if (mCapturedTrack && mCapturedTrack->GetKind() == Track::Note) {
|
|
// ((NoteTrack *) mCapturedTrack)->VScroll(mZoomStart, mZoomEnd);
|
|
// }
|
|
//#endif
|
|
Refresh(false);
|
|
}
|
|
}
|
|
|
|
/// VZoom Button up.
|
|
/// There are three cases:
|
|
/// - Drag-zooming; we already have a min and max
|
|
/// - Zoom out; ensure we don't go too small.
|
|
/// - Zoom in; ensure we don't go too large.
|
|
void TrackPanel::HandleVZoomButtonUp( wxMouseEvent & event )
|
|
{
|
|
if (!mCapturedTrack)
|
|
return;
|
|
|
|
mMouseCapture = IsUncaptured;
|
|
|
|
#ifdef USE_MIDI
|
|
// handle vertical scrolling in Note Track. This is so different from
|
|
// zooming in audio tracks that it is handled as a special case from
|
|
// which we then return
|
|
if (mCapturedTrack->GetKind() == Track::Note) {
|
|
NoteTrack *nt = (NoteTrack *) mCapturedTrack;
|
|
if (IsDragZooming()) {
|
|
nt->ZoomTo(mZoomStart, mZoomEnd);
|
|
} else if (event.ShiftDown() || event.RightUp()) {
|
|
nt->ZoomOut(mZoomEnd);
|
|
} else {
|
|
nt->ZoomIn(mZoomEnd);
|
|
}
|
|
mZoomEnd = mZoomStart = 0;
|
|
Refresh(false);
|
|
mCapturedTrack = NULL;
|
|
MakeParentModifyState(true);
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
|
|
// don't do anything if track is not wave
|
|
if (mCapturedTrack->GetKind() != Track::Wave)
|
|
return;
|
|
|
|
if (event.RightUp() &&
|
|
!(event.ShiftDown() || event.CmdDown())) {
|
|
OnVRulerMenu(mCapturedTrack, &event);
|
|
return;
|
|
}
|
|
|
|
HandleWaveTrackVZoom(static_cast<WaveTrack*>(mCapturedTrack),
|
|
event.ShiftDown(), event.RightUp());
|
|
mCapturedTrack = NULL;
|
|
}
|
|
|
|
void TrackPanel::HandleWaveTrackVZoom(WaveTrack *track, bool shiftDown, bool rightUp)
|
|
{
|
|
WaveTrack *partner = static_cast<WaveTrack *>(mTracks->GetLink(track));
|
|
int height = track->GetHeight();
|
|
int ypos = mCapturedRect.y;
|
|
|
|
// Ensure start and end are in order (swap if not).
|
|
if (mZoomEnd < mZoomStart) {
|
|
int temp = mZoomEnd;
|
|
mZoomEnd = mZoomStart;
|
|
mZoomStart = temp;
|
|
}
|
|
|
|
float min, max, c, l, minBand = 0;
|
|
const double rate = track->GetRate();
|
|
const float halfrate = rate / 2;
|
|
const SpectrogramSettings &settings = track->GetSpectrogramSettings();
|
|
NumberScale scale(track->GetSpectrogramSettings().GetScale(rate, false));
|
|
const bool spectral = (track->GetDisplay() == WaveTrack::Spectrum);
|
|
const bool spectrumLinear = spectral &&
|
|
(track->GetSpectrogramSettings().scaleType == SpectrogramSettings::stLinear);
|
|
|
|
if (spectral) {
|
|
if (spectrumLinear) {
|
|
min = settings.GetMinFreq(rate);
|
|
max = settings.GetMaxFreq(rate);
|
|
}
|
|
else {
|
|
min = settings.GetLogMinFreq(rate);
|
|
max = settings.GetLogMaxFreq(rate);
|
|
}
|
|
const int fftLength = settings.GetFFTLength();
|
|
const float binSize = rate / fftLength;
|
|
const int minBins =
|
|
std::min(10, fftLength / 2); //minimum 10 freq bins, unless there are less
|
|
minBand = minBins * binSize;
|
|
}
|
|
else
|
|
track->GetDisplayBounds(&min, &max);
|
|
|
|
if (IsDragZooming()) {
|
|
// Drag Zoom
|
|
const float tmin = min, tmax = max;
|
|
|
|
if (spectral) {
|
|
double xmin = 1 - (mZoomEnd - ypos) / (float)height;
|
|
double xmax = 1 - (mZoomStart - ypos) / (float)height;
|
|
const float middle = (xmin + xmax) / 2;
|
|
const float middleValue = scale.PositionToValue(middle);
|
|
|
|
min = std::max(spectrumLinear ? 0.0f : 1.0f,
|
|
std::min(middleValue - minBand / 2,
|
|
scale.PositionToValue(xmin)
|
|
));
|
|
max = std::min(halfrate,
|
|
std::max(middleValue + minBand / 2,
|
|
scale.PositionToValue(xmax)
|
|
));
|
|
}
|
|
else {
|
|
const float p1 = (mZoomStart - ypos) / (float)height;
|
|
const float p2 = (mZoomEnd - ypos) / (float)height;
|
|
max = (tmax * (1.0-p1) + tmin * p1);
|
|
min = (tmax * (1.0-p2) + tmin * p2);
|
|
|
|
// Waveform view - allow zooming down to a range of ZOOMLIMIT
|
|
if (max - min < ZOOMLIMIT) { // if user attempts to go smaller...
|
|
c = (min+max)/2; // ...set centre of view to centre of dragged area and top/bottom to ZOOMLIMIT/2 above/below
|
|
min = c - ZOOMLIMIT/2.0;
|
|
max = c + ZOOMLIMIT/2.0;
|
|
}
|
|
}
|
|
}
|
|
else if (shiftDown || rightUp) {
|
|
// Zoom OUT
|
|
if (spectral) {
|
|
if (shiftDown && rightUp) {
|
|
// Zoom out full
|
|
min = spectrumLinear ? 0.0f : 1.0f;
|
|
max = halfrate;
|
|
}
|
|
else {
|
|
// Zoom out
|
|
|
|
// (Used to zoom out centered at midline, ignoring the click, if linear view.
|
|
// I think it is better to be consistent. PRL)
|
|
// Center zoom-out at the midline
|
|
const float middle = // spectrumLinear ? 0.5f :
|
|
1.0f - (mZoomStart - ypos) / (float)height;
|
|
|
|
min = std::max(spectrumLinear ? 0.0f : 1.0f, scale.PositionToValue(middle - 1.0f));
|
|
max = std::min(halfrate, scale.PositionToValue(middle + 1.0f));
|
|
}
|
|
}
|
|
else {
|
|
// Zoom out to -1.0...1.0 first, then, and only
|
|
// then, if they click again, allow one more
|
|
// zoom out.
|
|
if (shiftDown && rightUp) {
|
|
// Zoom out full
|
|
min = -1.0;
|
|
max = 1.0;
|
|
}
|
|
else {
|
|
// Zoom out
|
|
if (min <= -1.0 && max >= 1.0) {
|
|
min = -2.0;
|
|
max = 2.0;
|
|
}
|
|
else {
|
|
c = 0.5*(min + max);
|
|
l = (c - min);
|
|
// limit to +/- 1 range unless already outside that range...
|
|
float minRange = (min < -1) ? -2.0 : -1.0;
|
|
float maxRange = (max > 1) ? 2.0 : 1.0;
|
|
// and enforce vertical zoom limits.
|
|
min = std::min(maxRange - ZOOMLIMIT, std::max(minRange, c - 2 * l));
|
|
max = std::max(minRange + ZOOMLIMIT, std::min(maxRange, c + 2 * l));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
// Zoom IN
|
|
if (spectral) {
|
|
// Center the zoom-in at the click
|
|
const float middle = 1.0f - (mZoomStart - ypos) / (float)height;
|
|
const float middleValue = scale.PositionToValue(middle);
|
|
|
|
min = std::max(spectrumLinear ? 0.0f : 1.0f,
|
|
std::min(middleValue - minBand / 2,
|
|
scale.PositionToValue(middle - 0.25f)
|
|
));
|
|
max = std::min(halfrate,
|
|
std::max(middleValue + minBand / 2,
|
|
scale.PositionToValue(middle + 0.25f)
|
|
));
|
|
}
|
|
else {
|
|
// Zoom in centered on cursor
|
|
float p1;
|
|
if (min < -1.0 || max > 1.0) {
|
|
min = -1.0;
|
|
max = 1.0;
|
|
}
|
|
else {
|
|
c = 0.5*(min + max);
|
|
// Enforce maximum vertical zoom
|
|
l = std::max(ZOOMLIMIT, (c - min));
|
|
|
|
p1 = (mZoomStart - ypos) / (float)height;
|
|
c = (max * (1.0 - p1) + min * p1);
|
|
min = c - 0.5*l;
|
|
max = c + 0.5*l;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (spectral) {
|
|
if (spectrumLinear) {
|
|
SpectrogramSettings &settings = track->GetSpectrogramSettings();
|
|
settings.SetMinFreq(min);
|
|
settings.SetMaxFreq(max);
|
|
if (partner) {
|
|
// To do: share memory with reference counting?
|
|
SpectrogramSettings &settings = partner->GetSpectrogramSettings();
|
|
settings.SetMinFreq(min);
|
|
settings.SetMaxFreq(max);
|
|
}
|
|
}
|
|
else {
|
|
SpectrogramSettings &settings = track->GetSpectrogramSettings();
|
|
settings.SetLogMinFreq(min);
|
|
settings.SetLogMaxFreq(max);
|
|
if (partner) {
|
|
// To do: share memory with reference counting?
|
|
SpectrogramSettings &settings = partner->GetSpectrogramSettings();
|
|
settings.SetLogMinFreq(min);
|
|
settings.SetLogMaxFreq(max);
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
track->SetDisplayBounds(min, max);
|
|
if (partner)
|
|
partner->SetDisplayBounds(min, max);
|
|
}
|
|
|
|
mZoomEnd = mZoomStart = 0;
|
|
UpdateVRuler(track);
|
|
Refresh(false);
|
|
MakeParentModifyState(true);
|
|
}
|
|
|
|
namespace {
|
|
// Is the sample horizontally nearest to the cursor sufficiently separated from
|
|
// its neighbors that the pencil tool should be allowed to drag it?
|
|
bool SampleResolutionTest(const ViewInfo &viewInfo, const WaveTrack *wt, double time, double rate)
|
|
{
|
|
// Require more than 3 pixels per sample
|
|
// Round to an exact sample time
|
|
const double adjustedTime = wt->LongSamplesToTime(wt->TimeToLongSamples(time));
|
|
const wxInt64 xx = std::max(wxInt64(0), viewInfo.TimeToPosition(adjustedTime));
|
|
ZoomInfo::Intervals intervals;
|
|
viewInfo.FindIntervals(rate, intervals);
|
|
ZoomInfo::Intervals::const_iterator it = intervals.begin(), end = intervals.end(), prev;
|
|
wxASSERT(it != end && it->position == 0);
|
|
do
|
|
prev = it++;
|
|
while (it != end && it->position <= xx);
|
|
const double threshold = 3 * rate; // three times as many pixels per second, as samples
|
|
return prev->averageZoom > threshold;
|
|
}
|
|
}
|
|
|
|
/// Determines if we can edit samples in a wave track.
|
|
/// Also pops up warning messages in certain cases where we can't.
|
|
/// @return true if we can edit the samples, false otherwise.
|
|
bool TrackPanel::IsSampleEditingPossible( wxMouseEvent &event, Track * t )
|
|
{
|
|
//Exit if we don't have a track
|
|
if(!t)
|
|
return false;
|
|
|
|
//Exit if it's not a WaveTrack
|
|
if(t->GetKind() != Track::Wave)
|
|
return false;
|
|
|
|
//Get out of here if we shouldn't be drawing right now:
|
|
//If we aren't displaying the waveform, Display a message dialog
|
|
WaveTrack *const wt = static_cast<WaveTrack*>(t);
|
|
const int display = wt->GetDisplay();
|
|
|
|
if (WaveTrack::Waveform != display)
|
|
{
|
|
wxMessageBox(_("To use Draw, choose 'Waveform' in the Track Drop-down Menu."), wxT("Draw Tool"));
|
|
return false;
|
|
}
|
|
|
|
bool showPoints;
|
|
{
|
|
wxRect r;
|
|
FindTrack(event.m_x, event.m_y, false, false, &r);
|
|
WaveTrack *const wt = static_cast<WaveTrack*>(t);
|
|
const double rate = wt->GetRate();
|
|
const double time = mViewInfo->PositionToTime(event.m_x, r.x);
|
|
showPoints = SampleResolutionTest(*mViewInfo, wt, time, rate);
|
|
}
|
|
|
|
//If we aren't zoomed in far enough, show a message dialog.
|
|
if(!showPoints)
|
|
{
|
|
wxMessageBox(_("To use Draw, zoom in further until you can see the individual samples."), wxT("Draw Tool"));
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
float TrackPanel::FindSampleEditingLevel(wxMouseEvent &event, double t0)
|
|
{
|
|
// Calculate where the mouse is located vertically (between +/- 1)
|
|
float zoomMin, zoomMax;
|
|
mDrawingTrack->GetDisplayBounds(&zoomMin, &zoomMax);
|
|
|
|
const int y = event.m_y - mDrawingTrackTop;
|
|
const int height = mDrawingTrack->GetHeight();
|
|
const bool dB = !mDrawingTrack->GetWaveformSettings().isLinear();
|
|
float newLevel =
|
|
::ValueOfPixel(y, height, false, dB, mViewInfo->dBr, zoomMin, zoomMax);
|
|
|
|
//Take the envelope into account
|
|
Envelope *const env = mDrawingTrack->GetEnvelopeAtX(event.m_x);
|
|
if (env)
|
|
{
|
|
double envValue = env->GetValue(t0);
|
|
if (envValue > 0)
|
|
newLevel /= envValue;
|
|
else
|
|
newLevel = 0;
|
|
|
|
//Make sure the new level is between +/-1
|
|
newLevel = std::max(-1.0f, std::min(1.0f, newLevel));
|
|
}
|
|
|
|
return newLevel;
|
|
}
|
|
|
|
/// We're in a track view and zoomed enough to see the samples.
|
|
/// Someone has just clicked the mouse. What do we do?
|
|
void TrackPanel::HandleSampleEditingClick( wxMouseEvent & event )
|
|
{
|
|
//declare a rectangle to determine clicking position
|
|
wxRect r;
|
|
|
|
//Get the track the mouse is over, and save it away for future events
|
|
mDrawingTrack = NULL;
|
|
Track *const t = FindTrack(event.m_x, event.m_y, false, false, &r);
|
|
|
|
if (!t || (t->GetKind() != Track::Wave))
|
|
return;
|
|
|
|
if( !IsSampleEditingPossible( event, t ) )
|
|
return;
|
|
|
|
/// \todo Should mCapturedTrack take the place of mDrawingTrack??
|
|
mDrawingTrack = static_cast<WaveTrack*>(t);
|
|
mDrawingTrackTop=r.y;
|
|
|
|
//If we are still around, we are drawing in earnest. Set some member data structures up:
|
|
//First, calculate the starting sample. To get this, we need the time
|
|
double t0 = mViewInfo->PositionToTime(event.m_x, GetLeftOffset());
|
|
|
|
//convert t0 to samples
|
|
mDrawingStartSample = mDrawingTrack->TimeToLongSamples(t0);
|
|
// quantize
|
|
t0 = mDrawingTrack->LongSamplesToTime(mDrawingStartSample);
|
|
|
|
//Determine how drawing should occur. If alt is down,
|
|
//do a smoothing, instead of redrawing.
|
|
if( event.m_altDown )
|
|
{
|
|
//*************************************************
|
|
//*** ALT-DOWN-CLICK (SAMPLE SMOOTHING) ***
|
|
//*************************************************
|
|
//
|
|
// Smoothing works like this: There is a smoothing kernel radius constant that
|
|
// determines how wide the averaging window is. Plus, there is a smoothing brush radius,
|
|
// which determines how many pixels wide around the selected pixel this smoothing is applied.
|
|
//
|
|
// Samples will be replaced by a mixture of the original points and the smoothed points,
|
|
// with a triangular mixing probability whose value at the center point is
|
|
// SMOOTHING_PROPORTION_MAX and at the far bounds is SMOOTHING_PROPORTION_MIN
|
|
|
|
//Get the region of samples around the selected point
|
|
int sampleRegionSize = 1 + 2 * (SMOOTHING_KERNEL_RADIUS + SMOOTHING_BRUSH_RADIUS);
|
|
float *sampleRegion = new float[sampleRegionSize];
|
|
float * newSampleRegion = new float[1 + 2 * SMOOTHING_BRUSH_RADIUS];
|
|
|
|
//Get a sample from the track to do some tricks on.
|
|
mDrawingTrack->Get((samplePtr)sampleRegion, floatSample,
|
|
(int)mDrawingStartSample - SMOOTHING_KERNEL_RADIUS - SMOOTHING_BRUSH_RADIUS,
|
|
sampleRegionSize);
|
|
int i, j;
|
|
|
|
//Go through each point of the smoothing brush and apply a smoothing operation.
|
|
for(j = -SMOOTHING_BRUSH_RADIUS; j <= SMOOTHING_BRUSH_RADIUS; j++){
|
|
float sumOfSamples = 0;
|
|
for (i= -SMOOTHING_KERNEL_RADIUS; i <= SMOOTHING_KERNEL_RADIUS; i++){
|
|
//Go through each point of the smoothing kernel and find the average
|
|
|
|
//The average is a weighted average, scaled by a weighting kernel that is simply triangular
|
|
// A triangular kernel across N items, with a radius of R ( 2 R + 1 points), if the farthest:
|
|
// points have a probability of a, the entire triangle has total probability of (R + 1)^2.
|
|
// For sample number i and middle brush sample M, (R + 1 - abs(M-i))/ ((R+1)^2) gives a
|
|
// legal distribution whose total probability is 1.
|
|
//
|
|
//
|
|
// weighting factor value
|
|
sumOfSamples += (SMOOTHING_KERNEL_RADIUS + 1 - abs(i)) * sampleRegion[i + j + SMOOTHING_KERNEL_RADIUS + SMOOTHING_BRUSH_RADIUS];
|
|
|
|
}
|
|
newSampleRegion[j + SMOOTHING_BRUSH_RADIUS] = sumOfSamples/((SMOOTHING_KERNEL_RADIUS + 1) *(SMOOTHING_KERNEL_RADIUS + 1) );
|
|
}
|
|
|
|
|
|
// Now that the new sample levels are determined, go through each and mix it appropriately
|
|
// with the original point, according to a 2-part linear function whose center has probability
|
|
// SMOOTHING_PROPORTION_MAX and extends out SMOOTHING_BRUSH_RADIUS, at which the probability is
|
|
// SMOOTHING_PROPORTION_MIN. _MIN and _MAX specify how much of the smoothed curve make it through.
|
|
|
|
float prob;
|
|
|
|
for(j=-SMOOTHING_BRUSH_RADIUS; j <= SMOOTHING_BRUSH_RADIUS; j++){
|
|
|
|
prob = SMOOTHING_PROPORTION_MAX - (float)abs(j)/SMOOTHING_BRUSH_RADIUS * (SMOOTHING_PROPORTION_MAX - SMOOTHING_PROPORTION_MIN);
|
|
|
|
newSampleRegion[j+SMOOTHING_BRUSH_RADIUS] =
|
|
newSampleRegion[j + SMOOTHING_BRUSH_RADIUS] * prob +
|
|
sampleRegion[SMOOTHING_BRUSH_RADIUS + SMOOTHING_KERNEL_RADIUS + j] * (1 - prob);
|
|
}
|
|
//Set the sample to the point of the mouse event
|
|
mDrawingTrack->Set((samplePtr)newSampleRegion, floatSample, mDrawingStartSample - SMOOTHING_BRUSH_RADIUS, 1 + 2 * SMOOTHING_BRUSH_RADIUS);
|
|
|
|
//Clean this up right away to avoid a memory leak
|
|
delete[] sampleRegion;
|
|
delete[] newSampleRegion;
|
|
|
|
mDrawingLastDragSampleValue = 0;
|
|
}
|
|
else
|
|
{
|
|
//*************************************************
|
|
//*** PLAIN DOWN-CLICK (NORMAL DRAWING) ***
|
|
//*************************************************
|
|
|
|
SetCapturedTrack(t, IsAdjustingSample);
|
|
|
|
//Otherwise (e.g., the alt button is not down) do normal redrawing, based on the mouse position.
|
|
const float newLevel = FindSampleEditingLevel(event, t0);
|
|
|
|
//Set the sample to the point of the mouse event
|
|
mDrawingTrack->Set((samplePtr)&newLevel, floatSample, mDrawingStartSample, 1);
|
|
|
|
mDrawingLastDragSampleValue = newLevel;
|
|
}
|
|
|
|
//Set the member data structures for drawing
|
|
mDrawingLastDragSample=mDrawingStartSample;
|
|
|
|
//Redraw the region of the selected track
|
|
RefreshTrack(mDrawingTrack);
|
|
}
|
|
|
|
void TrackPanel::HandleSampleEditingDrag( wxMouseEvent & event )
|
|
{
|
|
//*************************************************
|
|
//*** DRAG-DRAWING ***
|
|
//*************************************************
|
|
|
|
//The following will happen on a drag or a down-click.
|
|
// The point should get re-drawn at the location of the mouse.
|
|
//Exit if the mDrawingTrack is null.
|
|
if( mDrawingTrack == NULL)
|
|
return;
|
|
|
|
//Exit dragging if the alt key is down--Don't allow left-right dragging for smoothing operation
|
|
if (mMouseCapture != IsAdjustingSample)
|
|
return;
|
|
|
|
sampleCount s0; //declare this for use below. It designates the sample number which to draw.
|
|
|
|
// Figure out what time the click was at
|
|
//Find the point that we want to redraw at. If the control button is down,
|
|
//adjust only the originally clicked-on sample
|
|
|
|
if( event.m_controlDown) {
|
|
//*************************************************
|
|
//*** CTRL-DOWN (Hold Initial Sample Constant ***
|
|
//*************************************************
|
|
|
|
s0 = mDrawingStartSample;
|
|
}
|
|
else {
|
|
//*************************************************
|
|
//*** Normal CLICK-drag (Normal drawing) ***
|
|
//*************************************************
|
|
|
|
//Otherwise, adjust the sample you are dragging over right now.
|
|
//convert this to samples
|
|
const double t = mViewInfo->PositionToTime(event.m_x, GetLeftOffset());
|
|
s0 = mDrawingTrack->TimeToLongSamples(t);
|
|
}
|
|
|
|
const double t0 = mDrawingTrack->LongSamplesToTime(s0);
|
|
|
|
//Otherwise, do normal redrawing, based on the mouse position.
|
|
// Calculate where the mouse is located vertically (between +/- 1)
|
|
|
|
const float newLevel = FindSampleEditingLevel(event, t0);
|
|
|
|
//Now, redraw all samples between current and last redrawn sample, inclusive
|
|
//Go from the smaller to larger sample.
|
|
const int start = std::min( s0, mDrawingLastDragSample);
|
|
const int end = std::max( s0, mDrawingLastDragSample);
|
|
const int size = end - start + 1;
|
|
if (size == 1) {
|
|
mDrawingTrack->Set((samplePtr)&newLevel, floatSample, start, size);
|
|
}
|
|
else {
|
|
std::vector<float> values(size);
|
|
for (sampleCount i = start; i <= end; ++i) {
|
|
//This interpolates each sample linearly:
|
|
values[i - start] =
|
|
mDrawingLastDragSampleValue + (newLevel - mDrawingLastDragSampleValue) *
|
|
(float)(i - mDrawingLastDragSample) / (s0 - mDrawingLastDragSample);
|
|
}
|
|
mDrawingTrack->Set((samplePtr)&values[0], floatSample, start, size);
|
|
}
|
|
|
|
//Update the member data structures.
|
|
mDrawingLastDragSample=s0;
|
|
mDrawingLastDragSampleValue = newLevel;
|
|
|
|
//Redraw the region of the selected track
|
|
RefreshTrack(mDrawingTrack);
|
|
}
|
|
|
|
void TrackPanel::HandleSampleEditingButtonUp( wxMouseEvent & WXUNUSED(event))
|
|
{
|
|
//*************************************************
|
|
//*** UP-CLICK (Finish drawing) ***
|
|
//*************************************************
|
|
SetCapturedTrack( NULL );
|
|
//On up-click, send the state to the undo stack
|
|
mDrawingTrack=NULL; //Set this to NULL so it will catch improper drag events.
|
|
MakeParentPushState(_("Moved Samples"),
|
|
_("Sample Edit"),
|
|
PUSH_CONSOLIDATE|PUSH_AUTOSAVE);
|
|
}
|
|
|
|
|
|
/// This handles adjusting individual samples by hand using the draw tool(s)
|
|
///
|
|
/// There are several member data structure for handling drawing:
|
|
/// - mDrawingTrack: keeps track of which track you clicked down on, so drawing doesn't
|
|
/// jump to a new track
|
|
/// - mDrawingTrackTop: The top position of the drawing track--makes drawing easier.
|
|
/// - mDrawingStartSample: The sample you clicked down on, so that you can hold it steady
|
|
/// - mDrawingLastDragSample: When drag-drawing, this keeps track of the last sample you dragged over,
|
|
/// so it can smoothly redraw samples that got skipped over
|
|
/// - mDrawingLastDragSampleValue: The value of the last
|
|
void TrackPanel::HandleSampleEditing(wxMouseEvent & event)
|
|
{
|
|
if (event.LeftDown() ) {
|
|
HandleSampleEditingClick( event);
|
|
} else if (mDrawingTrack && event.Dragging()) {
|
|
HandleSampleEditingDrag( event );
|
|
} else if(mDrawingTrack && event.ButtonUp()) {
|
|
HandleSampleEditingButtonUp( event );
|
|
}
|
|
}
|
|
|
|
|
|
// This is for when a given track gets the x.
|
|
void TrackPanel::HandleClosing(wxMouseEvent & event)
|
|
{
|
|
Track *t = mCapturedTrack;
|
|
wxRect r = mCapturedRect;
|
|
|
|
wxRect closeRect;
|
|
mTrackInfo.GetCloseBoxRect(r, closeRect);
|
|
|
|
wxClientDC dc(this);
|
|
|
|
if (event.Dragging())
|
|
mTrackInfo.DrawCloseBox(&dc, r, closeRect.Contains(event.m_x, event.m_y));
|
|
else if (event.LeftUp()) {
|
|
mTrackInfo.DrawCloseBox(&dc, r, false);
|
|
if (closeRect.Contains(event.m_x, event.m_y)) {
|
|
if (!IsUnsafe())
|
|
RemoveTrack(t);
|
|
}
|
|
SetCapturedTrack( NULL );
|
|
}
|
|
|
|
this->UpdateViewIfNoTracks();
|
|
this->Refresh(false);
|
|
}
|
|
|
|
void TrackPanel::UpdateViewIfNoTracks()
|
|
{
|
|
if (mTracks->IsEmpty())
|
|
{
|
|
// BG: There are no more tracks on screen
|
|
//BG: Set zoom to normal
|
|
mViewInfo->SetZoom(ZoomInfo::GetDefaultZoom());
|
|
|
|
//STM: Set selection to 0,0
|
|
//PRL: and default the rest of the selection information
|
|
mViewInfo->selectedRegion = SelectedRegion();
|
|
|
|
// PRL: Following causes the time ruler to align 0 with left edge.
|
|
// Bug 972
|
|
mViewInfo->h = 0;
|
|
|
|
mListener->TP_RedrawScrollbars();
|
|
mListener->TP_DisplayStatusMessage(wxT("")); //STM: Clear message if all tracks are removed
|
|
}
|
|
}
|
|
|
|
/// Removes the specified track. Called from HandleClosing.
|
|
void TrackPanel::RemoveTrack(Track * toRemove)
|
|
{
|
|
// If it was focused, reassign focus to the next or, if
|
|
// unavailable, the previous track.
|
|
if (GetFocusedTrack() == toRemove) {
|
|
Track *t = mTracks->GetNext(toRemove, true);
|
|
if (t == NULL) {
|
|
t = mTracks->GetPrev( toRemove, true );
|
|
}
|
|
SetFocusedTrack(t); // It's okay if this is NULL
|
|
}
|
|
|
|
wxString name = toRemove->GetName();
|
|
Track *partner = toRemove->GetLink();
|
|
|
|
if (toRemove->GetKind() == Track::Wave)
|
|
{
|
|
// Update mixer board displayed tracks.
|
|
MixerBoard* pMixerBoard = this->GetMixerBoard();
|
|
if (pMixerBoard)
|
|
pMixerBoard->RemoveTrackCluster((WaveTrack*)toRemove); // Will remove partner shown in same cluster.
|
|
}
|
|
|
|
mTracks->Remove(toRemove, true);
|
|
if (partner) {
|
|
mTracks->Remove(partner, true);
|
|
}
|
|
|
|
if (mTracks->IsEmpty()) {
|
|
SetFocusedTrack( NULL );
|
|
}
|
|
|
|
MakeParentPushState(
|
|
wxString::Format(_("Removed track '%s.'"),
|
|
name.c_str()),
|
|
_("Track Remove"));
|
|
MakeParentRedrawScrollbars();
|
|
MakeParentResize();
|
|
Refresh(false);
|
|
}
|
|
|
|
void TrackPanel::HandlePopping(wxMouseEvent & event)
|
|
{
|
|
Track *t = mCapturedTrack;
|
|
wxRect r = mCapturedRect;
|
|
|
|
if( t==NULL ){
|
|
SetCapturedTrack( NULL );
|
|
return;
|
|
}
|
|
|
|
wxRect titleRect;
|
|
mTrackInfo.GetTitleBarRect(r, titleRect);
|
|
|
|
wxClientDC dc(this);
|
|
|
|
if (event.Dragging()) {
|
|
mTrackInfo.DrawTitleBar(&dc, r, t, titleRect.Contains(event.m_x, event.m_y));
|
|
}
|
|
else if (event.LeftUp()) {
|
|
if (titleRect.Contains(event.m_x, event.m_y))
|
|
{
|
|
OnTrackMenu(t);
|
|
}
|
|
|
|
SetCapturedTrack( NULL );
|
|
|
|
mTrackInfo.DrawTitleBar(&dc, r, t, false);
|
|
}
|
|
}
|
|
|
|
/// Handle when the mute or solo button is pressed for some track.
|
|
void TrackPanel::HandleMutingSoloing(wxMouseEvent & event, bool solo)
|
|
{
|
|
Track *t = mCapturedTrack;
|
|
wxRect r = mCapturedRect;
|
|
|
|
if( t==NULL ){
|
|
wxASSERT(false);// Soloing or muting but no captured track!
|
|
SetCapturedTrack( NULL );
|
|
return;
|
|
}
|
|
|
|
wxRect buttonRect;
|
|
mTrackInfo.GetMuteSoloRect(r, buttonRect, solo, HasSoloButton());
|
|
|
|
wxClientDC dc(this);
|
|
|
|
if (event.Dragging()){
|
|
mTrackInfo.DrawMuteSolo(&dc, r, t, buttonRect.Contains(event.m_x, event.m_y),
|
|
solo, HasSoloButton());
|
|
}
|
|
else if (event.LeftUp() )
|
|
{
|
|
if (buttonRect.Contains(event.m_x, event.m_y))
|
|
{
|
|
// For either, MakeParentPushState to make the track state dirty.
|
|
if(solo)
|
|
OnTrackSolo(event.ShiftDown(),t);
|
|
else
|
|
OnTrackMute(event.ShiftDown(),t);
|
|
}
|
|
SetCapturedTrack( NULL );
|
|
// mTrackInfo.DrawMuteSolo(&dc, r, t, false, solo);
|
|
Refresh(false);
|
|
}
|
|
}
|
|
|
|
void TrackPanel::HandleMinimizing(wxMouseEvent & event)
|
|
{
|
|
Track *t = mCapturedTrack;
|
|
wxRect r = mCapturedRect;
|
|
|
|
if (t == NULL) {
|
|
SetCapturedTrack(NULL);
|
|
return;
|
|
}
|
|
|
|
wxRect buttonRect;
|
|
mTrackInfo.GetMinimizeRect(r, buttonRect);
|
|
|
|
wxClientDC dc(this);
|
|
|
|
if (event.Dragging()) {
|
|
mTrackInfo.DrawMinimize(&dc, r, t, buttonRect.Contains(event.m_x, event.m_y));
|
|
}
|
|
else if (event.LeftUp()) {
|
|
if (buttonRect.Contains(event.m_x, event.m_y)) {
|
|
t->SetMinimized(!t->GetMinimized());
|
|
if (mTracks->GetLink(t))
|
|
mTracks->GetLink(t)->SetMinimized(t->GetMinimized());
|
|
MakeParentRedrawScrollbars();
|
|
MakeParentModifyState(true);
|
|
}
|
|
|
|
SetCapturedTrack(NULL);
|
|
|
|
mTrackInfo.DrawMinimize(&dc, r, t, false);
|
|
Refresh(false);
|
|
GetActiveProject()->RedrawProject();
|
|
}
|
|
}
|
|
|
|
void TrackPanel::HandleSliders(wxMouseEvent &event, bool pan)
|
|
{
|
|
LWSlider *slider;
|
|
#ifdef EXPERIMENTAL_OUTPUT_DISPLAY
|
|
bool panZero = false;
|
|
#endif
|
|
|
|
// On the Mac, we'll lose track capture if the slider dialog
|
|
// is displayed, but it doesn't hurt to do this for all plats.
|
|
WaveTrack *capturedTrack = (WaveTrack *) mCapturedTrack;
|
|
|
|
if (pan)
|
|
slider = mTrackInfo.PanSlider(capturedTrack);
|
|
else
|
|
slider = mTrackInfo.GainSlider(capturedTrack);
|
|
|
|
slider->OnMouseEvent(event);
|
|
|
|
//If we have a double-click, do this...
|
|
if (event.LeftDClick())
|
|
mMouseCapture = IsUncaptured;
|
|
|
|
float newValue = slider->Get();
|
|
MixerBoard* pMixerBoard = this->GetMixerBoard(); // Update mixer board, too.
|
|
#ifdef EXPERIMENTAL_MIDI_OUT
|
|
if (capturedTrack->GetKind() == Track::Wave) {
|
|
#endif
|
|
WaveTrack *link = (WaveTrack *)mTracks->GetLink(capturedTrack);
|
|
|
|
if (pan) {
|
|
#ifdef EXPERIMENTAL_OUTPUT_DISPLAY
|
|
panZero = ((WaveTrack *)capturedTrack)->SetPan(newValue);
|
|
#else
|
|
((WaveTrack *)capturedTrack)->SetPan(newValue);
|
|
#endif
|
|
if (link)
|
|
link->SetPan(newValue);
|
|
|
|
#ifdef EXPERIMENTAL_OUTPUT_DISPLAY
|
|
if(panZero) MakeParentRedrawScrollbars();
|
|
#endif
|
|
|
|
if (pMixerBoard)
|
|
pMixerBoard->UpdatePan((WaveTrack*)capturedTrack);
|
|
}
|
|
else {
|
|
((WaveTrack *)capturedTrack)->SetGain(newValue);
|
|
if (link)
|
|
link->SetGain(newValue);
|
|
|
|
if (pMixerBoard)
|
|
pMixerBoard->UpdateGain((WaveTrack*)capturedTrack);
|
|
}
|
|
#ifdef EXPERIMENTAL_MIDI_OUT
|
|
} else { // Note: funny indentation to match "if" about 20 lines back
|
|
if (!pan) {
|
|
((NoteTrack *) capturedTrack)->SetGain(newValue);
|
|
#ifdef EXPERIMENTAL_MIXER_BOARD
|
|
if (pMixerBoard)
|
|
// probably should modify UpdateGain to take a track that is
|
|
// either a WaveTrack or a NoteTrack.
|
|
pMixerBoard->UpdateGain((WaveTrack*)capturedTrack);
|
|
#endif
|
|
}
|
|
}
|
|
#endif
|
|
|
|
RefreshTrack(capturedTrack);
|
|
|
|
if (event.ButtonUp()) {
|
|
#ifdef EXPERIMENTAL_MIDI_OUT
|
|
if (capturedTrack->GetKind() == Track::Wave) {
|
|
#endif
|
|
MakeParentPushState(pan ? _("Moved pan slider") : _("Moved gain slider"),
|
|
pan ? _("Pan") : _("Gain"),
|
|
PUSH_CONSOLIDATE);
|
|
#ifdef EXPERIMENTAL_MIDI_OUT
|
|
} else {
|
|
MakeParentPushState(_("Moved velocity slider"), _("Velocity"), true);
|
|
}
|
|
#endif
|
|
SetCapturedTrack( NULL );
|
|
}
|
|
}
|
|
|
|
// The tracks positions within the list have changed, so update the vertical
|
|
// ruler size for the track that triggered the event.
|
|
void TrackPanel::OnTrackListResized(wxCommandEvent & e)
|
|
{
|
|
Track *t = (Track *) e.GetClientData();
|
|
|
|
UpdateVRuler(t);
|
|
|
|
e.Skip();
|
|
}
|
|
|
|
// Tracks have been added or removed from the list. Handle adds as if
|
|
// a resize has taken place.
|
|
void TrackPanel::OnTrackListUpdated(wxCommandEvent & e)
|
|
{
|
|
// Tracks may have been deleted, so check to see if the focused track was on of them.
|
|
if (!mTracks->Contains(GetFocusedTrack())) {
|
|
SetFocusedTrack(NULL);
|
|
}
|
|
|
|
if (e.GetClientData()) {
|
|
OnTrackListResized(e);
|
|
return;
|
|
}
|
|
|
|
e.Skip();
|
|
}
|
|
|
|
void TrackPanel::OnContextMenu(wxContextMenuEvent & WXUNUSED(event))
|
|
{
|
|
OnTrackMenu();
|
|
}
|
|
|
|
/// This handles when the user clicks on the "Label" area
|
|
/// of a track, ie the part with all the buttons and the drop
|
|
/// down menu, etc.
|
|
// That is, TrackInfo and vertical ruler rect.
|
|
void TrackPanel::HandleLabelClick(wxMouseEvent & event)
|
|
{
|
|
// AS: If not a click, ignore the mouse event.
|
|
if (!event.ButtonDown() && !event.ButtonDClick()) {
|
|
return;
|
|
}
|
|
|
|
// MIDI tracks use the right mouse button, but other tracks get confused
|
|
// if they see anything other than a left click.
|
|
bool isleft = event.Button(wxMOUSE_BTN_LEFT);
|
|
|
|
bool unsafe = IsUnsafe();
|
|
|
|
wxRect r;
|
|
|
|
Track *t = FindTrack(event.m_x, event.m_y, true, true, &r);
|
|
|
|
// AS: If the user clicked outside all tracks, make nothing
|
|
// selected.
|
|
if (!t) {
|
|
SelectNone();
|
|
Refresh(false);
|
|
return;
|
|
}
|
|
|
|
// LL: Check close box
|
|
if (isleft && CloseFunc(t, r, event.m_x, event.m_y))
|
|
return;
|
|
|
|
// LL: Check title bar for popup
|
|
if (isleft && PopupFunc(t, r, event.m_x, event.m_y))
|
|
return;
|
|
|
|
// VJ: Check sync-lock icon and the blank area to the left of the minimize button.
|
|
// Have to do it here, because if track is shrunk such that these areas occlude controls,
|
|
// e.g., mute/solo, don't want the "Funcs" below to set up handling.
|
|
// Only result of doing so is to select the track. Don't care whether isleft.
|
|
bool bTrackSelClick = this->TrackSelFunc(t, r, event.m_x, event.m_y);
|
|
if (!bTrackSelClick)
|
|
{
|
|
// MM: Check minimize buttons on WaveTracks. Must be before
|
|
// solo/mute buttons, sliders etc.
|
|
if (isleft && MinimizeFunc(t, r, event.m_x, event.m_y))
|
|
return;
|
|
|
|
if (isleft && t->GetKind() == Track::Wave)
|
|
{
|
|
// DM: Check Mute and Solo buttons on WaveTracks:
|
|
if (MuteSoloFunc(t, r, event.m_x, event.m_y, false) ||
|
|
MuteSoloFunc(t, r, event.m_x, event.m_y, true))
|
|
return;
|
|
|
|
if (GainFunc(t, r, event, event.m_x, event.m_y))
|
|
return;
|
|
|
|
if (PanFunc(t, r, event, event.m_x, event.m_y))
|
|
return;
|
|
}
|
|
#ifdef USE_MIDI
|
|
// DM: If it's a NoteTrack, it has special controls
|
|
else if (t->GetKind() == Track::Note)
|
|
{
|
|
wxRect midiRect;
|
|
#ifdef EXPERIMENTAL_MIDI_OUT
|
|
// this is an awful hack: make a new rectangle at an offset because
|
|
// MuteSoloFunc thinks buttons are located below some text, e.g.
|
|
// "Mono, 44100Hz 32-bit float", but this is not true for a Note track
|
|
wxRect muteSoloRect(r);
|
|
muteSoloRect.y -= 34; // subtract the height of wave track text
|
|
if (MuteSoloFunc(t, muteSoloRect, event.m_x, event.m_y, false) ||
|
|
MuteSoloFunc(t, muteSoloRect, event.m_x, event.m_y, true))
|
|
return;
|
|
|
|
// this is a similar hack: GainFunc expects a Wave track slider, so it's
|
|
// looking in the wrong place. We pass it a bogus rectangle created when
|
|
// the slider was placed to "fake" GainFunc into finding the slider in
|
|
// its actual location.
|
|
if (GainFunc(t, ((NoteTrack *) t)->GetGainPlacementRect(),
|
|
event, event.m_x, event.m_y))
|
|
return;
|
|
#endif
|
|
mTrackInfo.GetTrackControlsRect(r, midiRect);
|
|
if (midiRect.Contains(event.m_x, event.m_y) &&
|
|
((NoteTrack *) t)->LabelClick(midiRect, event.m_x, event.m_y,
|
|
event.Button(wxMOUSE_BTN_RIGHT))) {
|
|
Refresh(false);
|
|
return;
|
|
}
|
|
}
|
|
#endif // USE_MIDI
|
|
|
|
if (!isleft) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
// DM: If they weren't clicking on a particular part of a track label,
|
|
// deselect other tracks and select this one.
|
|
|
|
// JH: also, capture the current track for rearranging, so the user
|
|
// can drag the track up or down to swap it with others
|
|
if (!unsafe) {
|
|
SetCapturedTrack( t, IsRearranging );
|
|
TrackPanel::CalculateRearrangingThresholds(event);
|
|
}
|
|
|
|
// AS: If the shift button is being held down, invert
|
|
// the selection on this track.
|
|
if (event.ShiftDown()) {
|
|
mTracks->Select(t, !t->GetSelected());
|
|
Refresh(false);
|
|
MixerBoard* pMixerBoard = this->GetMixerBoard();
|
|
if (pMixerBoard && (t->GetKind() == Track::Wave))
|
|
pMixerBoard->RefreshTrackCluster((WaveTrack*)t);
|
|
return;
|
|
}
|
|
|
|
SelectNone();
|
|
mTracks->Select(t);
|
|
SetFocusedTrack(t);
|
|
SelectTrackLength(t);
|
|
|
|
this->Refresh(false);
|
|
MixerBoard* pMixerBoard = this->GetMixerBoard();
|
|
if (pMixerBoard)
|
|
pMixerBoard->RefreshTrackClusters();
|
|
|
|
if (!unsafe)
|
|
MakeParentModifyState(true);
|
|
}
|
|
|
|
/// The user is dragging one of the tracks: change the track order
|
|
/// accordingly
|
|
void TrackPanel::HandleRearrange(wxMouseEvent & event)
|
|
{
|
|
// are we finishing the drag?
|
|
if (event.LeftUp()) {
|
|
SetCapturedTrack( NULL );
|
|
SetCursor(*mArrowCursor);
|
|
return;
|
|
}
|
|
|
|
// probably harmless during play? However, we do disallow the click, so check this too.
|
|
bool unsafe = IsUnsafe();
|
|
if (unsafe)
|
|
return;
|
|
|
|
MixerBoard* pMixerBoard = this->GetMixerBoard(); // Update mixer board, too.
|
|
wxString dir;
|
|
if (event.m_y < mMoveUpThreshold || event.m_y < 0) {
|
|
mTracks->MoveUp(mCapturedTrack);
|
|
dir = _("up");
|
|
#ifdef EXPERIMENTAL_MIDI_OUT
|
|
if (pMixerBoard && (mCapturedTrack->GetKind() == Track::Wave ||
|
|
mCapturedTrack->GetKind() == Track::Note))
|
|
pMixerBoard->MoveTrackCluster(mCapturedTrack, true /* up */);
|
|
#else
|
|
if (pMixerBoard && (mCapturedTrack->GetKind() == Track::Wave))
|
|
pMixerBoard->MoveTrackCluster((WaveTrack*)mCapturedTrack, true /* up */);
|
|
#endif
|
|
}
|
|
else if (event.m_y > mMoveDownThreshold || event.m_y > GetRect().GetHeight()) {
|
|
mTracks->MoveDown(mCapturedTrack);
|
|
/* i18n-hint: a direction as in up or down.*/
|
|
dir = _("down");
|
|
#ifdef EXPERIMENTAL_MIDI_OUT
|
|
if (pMixerBoard && (mCapturedTrack->GetKind() == Track::Wave ||
|
|
mCapturedTrack->GetKind() == Track::Note))
|
|
pMixerBoard->MoveTrackCluster(mCapturedTrack, false /* down */);
|
|
#else
|
|
if (pMixerBoard && (mCapturedTrack->GetKind() == Track::Wave))
|
|
pMixerBoard->MoveTrackCluster((WaveTrack*)mCapturedTrack, false /* down */);
|
|
#endif
|
|
}
|
|
else
|
|
{
|
|
return;
|
|
}
|
|
|
|
MakeParentPushState(wxString::Format(_("Moved '%s' %s"),
|
|
mCapturedTrack->GetName().c_str(),
|
|
dir.c_str()),
|
|
_("Move Track"));
|
|
|
|
// JH: if we moved up or down, recalculate the thresholds and make sure the
|
|
// track is fully on-screen.
|
|
TrackPanel::CalculateRearrangingThresholds(event);
|
|
EnsureVisible(mCapturedTrack);
|
|
}
|
|
|
|
/// Figure out how far the user must drag the mouse up or down
|
|
/// before the track will swap with the one above or below
|
|
void TrackPanel::CalculateRearrangingThresholds(wxMouseEvent & event)
|
|
{
|
|
wxASSERT(mCapturedTrack);
|
|
|
|
// JH: this will probably need to be tweaked a bit, I'm just
|
|
// not sure what formula will have the best feel for the
|
|
// user.
|
|
if (mTracks->CanMoveUp(mCapturedTrack))
|
|
mMoveUpThreshold =
|
|
event.m_y - mTracks->GetGroupHeight( mTracks->GetPrev(mCapturedTrack,true) );
|
|
else
|
|
mMoveUpThreshold = INT_MIN;
|
|
|
|
if (mTracks->CanMoveDown(mCapturedTrack))
|
|
mMoveDownThreshold =
|
|
event.m_y + mTracks->GetGroupHeight( mTracks->GetNext(mCapturedTrack,true) );
|
|
else
|
|
mMoveDownThreshold = INT_MAX;
|
|
}
|
|
|
|
bool TrackPanel::GainFunc(Track * t, wxRect r, wxMouseEvent &event,
|
|
int x, int y)
|
|
{
|
|
wxRect sliderRect;
|
|
mTrackInfo.GetGainRect(r, sliderRect);
|
|
if (!sliderRect.Contains(x, y))
|
|
return false;
|
|
|
|
SetCapturedTrack( t, IsGainSliding);
|
|
mCapturedRect = r;
|
|
HandleSliders(event, false);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool TrackPanel::PanFunc(Track * t, wxRect r, wxMouseEvent &event,
|
|
int x, int y)
|
|
{
|
|
wxRect sliderRect;
|
|
mTrackInfo.GetPanRect(r, sliderRect);
|
|
if (!sliderRect.Contains(x, y))
|
|
return false;
|
|
|
|
SetCapturedTrack( t, IsPanSliding);
|
|
mCapturedRect = r;
|
|
HandleSliders(event, true);
|
|
|
|
return true;
|
|
}
|
|
|
|
/// Mute or solo the given track (t). If solo is true, we're
|
|
/// soloing, otherwise we're muting. Basically, check and see
|
|
/// whether x and y fall within the area of the appropriate button.
|
|
bool TrackPanel::MuteSoloFunc(Track * t, wxRect r, int x, int y,
|
|
bool solo)
|
|
{
|
|
wxRect buttonRect;
|
|
mTrackInfo.GetMuteSoloRect(r, buttonRect, solo, HasSoloButton());
|
|
if (!buttonRect.Contains(x, y))
|
|
return false;
|
|
|
|
wxClientDC dc(this);
|
|
SetCapturedTrack( t, solo ? IsSoloing : IsMuting);
|
|
mCapturedRect = r;
|
|
|
|
mTrackInfo.DrawMuteSolo(&dc, r, t, true, solo, HasSoloButton());
|
|
return true;
|
|
}
|
|
|
|
bool TrackPanel::TrackSelFunc(Track * WXUNUSED(t), wxRect r, int x, int y)
|
|
{
|
|
// First check the blank space to left of minimize button.
|
|
wxRect selRect;
|
|
mTrackInfo.GetMinimizeRect(r, selRect); // for y and height
|
|
selRect.x = r.x;
|
|
selRect.width = 16; // (kTrackInfoBtnSize)
|
|
selRect.height++;
|
|
if (selRect.Contains(x, y))
|
|
return true;
|
|
|
|
// Try the sync-lock rect.
|
|
mTrackInfo.GetSyncLockIconRect(r, selRect);
|
|
selRect.height++;
|
|
return selRect.Contains(x, y);
|
|
}
|
|
|
|
bool TrackPanel::MinimizeFunc(Track * t, wxRect r, int x, int y)
|
|
{
|
|
wxRect buttonRect;
|
|
mTrackInfo.GetMinimizeRect(r, buttonRect);
|
|
if (!buttonRect.Contains(x, y))
|
|
return false;
|
|
|
|
SetCapturedTrack(t, IsMinimizing);
|
|
mCapturedRect = r;
|
|
|
|
wxClientDC dc(this);
|
|
mTrackInfo.DrawMinimize(&dc, r, t, true);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool TrackPanel::CloseFunc(Track * t, wxRect r, int x, int y)
|
|
{
|
|
wxRect closeRect;
|
|
mTrackInfo.GetCloseBoxRect(r, closeRect);
|
|
|
|
if (!closeRect.Contains(x, y))
|
|
return false;
|
|
|
|
wxClientDC dc(this);
|
|
SetCapturedTrack( t, IsClosing );
|
|
mCapturedRect = r;
|
|
|
|
mTrackInfo.DrawCloseBox(&dc, r, true);
|
|
return true;
|
|
}
|
|
|
|
bool TrackPanel::PopupFunc(Track * t, wxRect r, int x, int y)
|
|
{
|
|
wxRect titleRect;
|
|
mTrackInfo.GetTitleBarRect(r, titleRect);
|
|
if (!titleRect.Contains(x, y))
|
|
return false;
|
|
|
|
wxClientDC dc(this);
|
|
SetCapturedTrack( t, IsPopping );
|
|
mCapturedRect = r;
|
|
|
|
mTrackInfo.DrawTitleBar(&dc, r, t, true);
|
|
return true;
|
|
}
|
|
|
|
/// ButtonDown means they just clicked and haven't released yet.
|
|
/// We use this opportunity to save which track they clicked on,
|
|
/// and the initial height of the track, so as they drag we can
|
|
/// update the track size.
|
|
void TrackPanel::HandleResizeClick( wxMouseEvent & event )
|
|
{
|
|
wxRect rTrack;
|
|
wxRect rLabel;
|
|
|
|
// DM: Figure out what track is about to be resized
|
|
Track *track = FindTrack(event.m_x, event.m_y, false, false, &rTrack);
|
|
|
|
if (!track) {
|
|
// This will only return unlinked tracks or left channels of stereo tracks
|
|
// or NULL:
|
|
track = FindTrack(event.m_x, event.m_y, true, true, &rLabel);
|
|
// If stereo, get the right channel.
|
|
if (track && track->GetLinked())
|
|
track = track->GetLink();
|
|
}
|
|
|
|
if (!track) {
|
|
return;
|
|
}
|
|
|
|
mMouseClickY = event.m_y;
|
|
|
|
#ifdef EXPERIMENTAL_OUTPUT_DISPLAY
|
|
// To do: escape key
|
|
if(MONO_WAVE_PAN(t)){
|
|
//STM: Determine whether we should rescale one or two tracks
|
|
if (t->GetVirtualStereo()) {
|
|
// mCapturedTrack is the lower track
|
|
mInitialTrackHeight = t->GetHeight(true);
|
|
mInitialUpperTrackHeight = t->GetHeight();
|
|
SetCapturedTrack(t, IsResizingBelowLinkedTracks);
|
|
}
|
|
else {
|
|
// mCapturedTrack is the upper track
|
|
mInitialTrackHeight = t->GetHeight(true);
|
|
mInitialUpperTrackHeight = t->GetHeight();
|
|
SetCapturedTrack(t, IsResizingBetweenLinkedTracks);
|
|
}
|
|
}else{
|
|
Track *prev = mTracks->GetPrev(t);
|
|
Track *next = mTracks->GetNext(t);
|
|
|
|
//STM: Determine whether we should rescale one or two tracks
|
|
if (prev && prev->GetLink() == t) {
|
|
// mCapturedTrack is the lower track
|
|
mInitialTrackHeight = t->GetHeight();
|
|
mInitialMinimized = t->GetMinimized();
|
|
mInitialUpperTrackHeight = prev->GetHeight();
|
|
SetCapturedTrack(t, IsResizingBelowLinkedTracks);
|
|
}
|
|
else if (next && t->GetLink() == next) {
|
|
// mCapturedTrack is the upper track
|
|
mInitialTrackHeight = next->GetHeight();
|
|
mInitialMinimized = next->GetMinimized();
|
|
mInitialUpperTrackHeight = t->GetHeight();
|
|
SetCapturedTrack(t, IsResizingBetweenLinkedTracks);
|
|
}
|
|
else {
|
|
// DM: Save the initial mouse location and the initial height
|
|
mInitialTrackHeight = t->GetHeight();
|
|
mInitialMinimized = t->GetMinimized();
|
|
SetCapturedTrack(t, IsResizing);
|
|
}
|
|
}
|
|
#else // EXPERIMENTAL_OUTPUT_DISPLAY
|
|
Track *prev = mTracks->GetPrev(track);
|
|
Track *next = mTracks->GetNext(track);
|
|
|
|
//STM: Determine whether we should rescale one or two tracks
|
|
if (prev && prev->GetLink() == track) {
|
|
// mCapturedTrack is the lower track
|
|
mInitialTrackHeight = track->GetHeight();
|
|
mInitialActualHeight = track->GetActualHeight();
|
|
mInitialMinimized = track->GetMinimized();
|
|
mInitialUpperTrackHeight = prev->GetHeight();
|
|
mInitialUpperActualHeight = prev->GetActualHeight();
|
|
SetCapturedTrack(track, IsResizingBelowLinkedTracks);
|
|
}
|
|
else if (next && track->GetLink() == next) {
|
|
// mCapturedTrack is the upper track
|
|
mInitialTrackHeight = next->GetHeight();
|
|
mInitialActualHeight = next->GetActualHeight();
|
|
mInitialMinimized = next->GetMinimized();
|
|
mInitialUpperTrackHeight = track->GetHeight();
|
|
mInitialUpperActualHeight = track->GetActualHeight();
|
|
SetCapturedTrack(track, IsResizingBetweenLinkedTracks);
|
|
}
|
|
else {
|
|
// DM: Save the initial mouse location and the initial height
|
|
mInitialTrackHeight = track->GetHeight();
|
|
mInitialActualHeight = track->GetActualHeight();
|
|
mInitialMinimized = track->GetMinimized();
|
|
SetCapturedTrack(track, IsResizing);
|
|
}
|
|
#endif // EXPERIMENTAL_OUTPUT_DISPLAY
|
|
}
|
|
|
|
/// This happens when the button is released from a drag.
|
|
/// Since we actually took care of resizing the track when
|
|
/// we got drag events, all we have to do here is clean up.
|
|
/// We also modify the undo state (the action doesn't become
|
|
/// undo-able, but it gets merged with the previous undo-able
|
|
/// event).
|
|
void TrackPanel::HandleResizeButtonUp(wxMouseEvent & WXUNUSED(event))
|
|
{
|
|
SetCapturedTrack( NULL );
|
|
MakeParentRedrawScrollbars();
|
|
MakeParentModifyState(false);
|
|
}
|
|
|
|
/// Resize dragging means that the mouse button IS down and has moved
|
|
/// from its initial location. By the time we get here, we
|
|
/// have already received a ButtonDown() event and saved the
|
|
/// track being resized in mCapturedTrack.
|
|
void TrackPanel::HandleResizeDrag(wxMouseEvent & event)
|
|
{
|
|
int delta = (event.m_y - mMouseClickY);
|
|
|
|
// On first drag, jump out of minimized mode. Initial height
|
|
// will be height of minimized track.
|
|
//
|
|
// This used to be in HandleResizeClick(), but simply clicking
|
|
// on a resize border would switch the minimized state.
|
|
if (mCapturedTrack->GetMinimized()) {
|
|
Track *link = mCapturedTrack->GetLink();
|
|
|
|
mCapturedTrack->SetHeight(mCapturedTrack->GetHeight());
|
|
mCapturedTrack->SetMinimized(false);
|
|
|
|
if (link) {
|
|
link->SetHeight(link->GetHeight());
|
|
link->SetMinimized(false);
|
|
// Initial values must be reset since they weren't based on the
|
|
// minimized heights.
|
|
mInitialUpperTrackHeight = link->GetHeight();
|
|
mInitialTrackHeight = mCapturedTrack->GetHeight();
|
|
}
|
|
#ifdef EXPERIMENTAL_OUTPUT_DISPLAY
|
|
else if(MONO_WAVE_PAN(mCapturedTrack)){
|
|
mCapturedTrack->SetMinimized(false);
|
|
mInitialUpperTrackHeight = mCapturedTrack->GetHeight();
|
|
mInitialTrackHeight = mCapturedTrack->GetHeight(true);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
//STM: We may be dragging one or two (stereo) tracks.
|
|
// If two, resize proportionally if we are dragging the lower track, and
|
|
// adjust compensatively if we are dragging the upper track.
|
|
#ifdef EXPERIMENTAL_OUTPUT_DISPLAY
|
|
switch( mMouseCapture )
|
|
{
|
|
case IsResizingBelowLinkedTracks:
|
|
{
|
|
if(MONO_WAVE_PAN(mCapturedTrack)){
|
|
double proportion = static_cast < double >(mInitialTrackHeight)
|
|
/ (mInitialTrackHeight + mInitialUpperTrackHeight);
|
|
|
|
int newTrackHeight = static_cast < int >
|
|
(mInitialTrackHeight + delta * proportion);
|
|
|
|
int newUpperTrackHeight = static_cast < int >
|
|
(mInitialUpperTrackHeight + delta * (1.0 - proportion));
|
|
|
|
//make sure neither track is smaller than its minimum height
|
|
if (newTrackHeight < mCapturedTrack->GetMinimizedHeight())
|
|
newTrackHeight = mCapturedTrack->GetMinimizedHeight();
|
|
if (newUpperTrackHeight < mCapturedTrack->GetMinimizedHeight())
|
|
newUpperTrackHeight = mCapturedTrack->GetMinimizedHeight();
|
|
|
|
mCapturedTrack->SetHeight(newTrackHeight,true);
|
|
mCapturedTrack->SetHeight(newUpperTrackHeight);
|
|
}
|
|
else{
|
|
Track *prev = mTracks->GetPrev(mCapturedTrack);
|
|
|
|
double proportion = static_cast < double >(mInitialTrackHeight)
|
|
/ (mInitialTrackHeight + mInitialUpperTrackHeight);
|
|
|
|
int newTrackHeight = static_cast < int >
|
|
(mInitialTrackHeight + delta * proportion);
|
|
|
|
int newUpperTrackHeight = static_cast < int >
|
|
(mInitialUpperTrackHeight + delta * (1.0 - proportion));
|
|
|
|
//make sure neither track is smaller than its minimum height
|
|
if (newTrackHeight < mCapturedTrack->GetMinimizedHeight())
|
|
newTrackHeight = mCapturedTrack->GetMinimizedHeight();
|
|
if (newUpperTrackHeight < prev->GetMinimizedHeight())
|
|
newUpperTrackHeight = prev->GetMinimizedHeight();
|
|
|
|
mCapturedTrack->SetHeight(newTrackHeight);
|
|
prev->SetHeight(newUpperTrackHeight);
|
|
}
|
|
break;
|
|
}
|
|
case IsResizingBetweenLinkedTracks:
|
|
{
|
|
if(MONO_WAVE_PAN(mCapturedTrack)){
|
|
int newUpperTrackHeight = mInitialUpperTrackHeight + delta;
|
|
int newTrackHeight = mInitialTrackHeight - delta;
|
|
|
|
// make sure neither track is smaller than its minimum height
|
|
if (newTrackHeight < mCapturedTrack->GetMinimizedHeight()) {
|
|
newTrackHeight = mCapturedTrack->GetMinimizedHeight();
|
|
newUpperTrackHeight =
|
|
mInitialUpperTrackHeight + mInitialTrackHeight - mCapturedTrack->GetMinimizedHeight();
|
|
}
|
|
if (newUpperTrackHeight < mCapturedTrack->GetMinimizedHeight()) {
|
|
newUpperTrackHeight = mCapturedTrack->GetMinimizedHeight();
|
|
newTrackHeight =
|
|
mInitialUpperTrackHeight + mInitialTrackHeight - mCapturedTrack->GetMinimizedHeight();
|
|
}
|
|
float temp = 1.0f;
|
|
if(newUpperTrackHeight != 0.0f)
|
|
temp = (float)newUpperTrackHeight/(float)(newUpperTrackHeight + newTrackHeight);
|
|
|
|
mCapturedTrack->SetVirtualTrackPercentage(temp);
|
|
mCapturedTrack->SetHeight(newUpperTrackHeight);
|
|
mCapturedTrack->SetHeight(newTrackHeight,true);
|
|
}
|
|
else{
|
|
Track *next = mTracks->GetNext(mCapturedTrack);
|
|
int newUpperTrackHeight = mInitialUpperTrackHeight + delta;
|
|
int newTrackHeight = mInitialTrackHeight - delta;
|
|
|
|
// make sure neither track is smaller than its minimum height
|
|
if (newTrackHeight < next->GetMinimizedHeight()) {
|
|
newTrackHeight = next->GetMinimizedHeight();
|
|
newUpperTrackHeight =
|
|
mInitialUpperTrackHeight + mInitialTrackHeight - next->GetMinimizedHeight();
|
|
}
|
|
if (newUpperTrackHeight < mCapturedTrack->GetMinimizedHeight()) {
|
|
newUpperTrackHeight = mCapturedTrack->GetMinimizedHeight();
|
|
newTrackHeight =
|
|
mInitialUpperTrackHeight + mInitialTrackHeight - mCapturedTrack->GetMinimizedHeight();
|
|
}
|
|
|
|
mCapturedTrack->SetHeight(newUpperTrackHeight);
|
|
next->SetHeight(newTrackHeight);
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
case IsResizing:
|
|
{
|
|
int newTrackHeight = mInitialTrackHeight + delta;
|
|
if (newTrackHeight < mCapturedTrack->GetMinimizedHeight())
|
|
newTrackHeight = mCapturedTrack->GetMinimizedHeight();
|
|
mCapturedTrack->SetHeight(newTrackHeight);
|
|
break;
|
|
}
|
|
default:
|
|
// don't refresh in this case.
|
|
return;
|
|
}
|
|
#else // EXPERIMENTAL_OUTPUT_DISPLAY
|
|
switch( mMouseCapture )
|
|
{
|
|
case IsResizingBelowLinkedTracks:
|
|
{
|
|
Track *prev = mTracks->GetPrev(mCapturedTrack);
|
|
|
|
double proportion = static_cast < double >(mInitialTrackHeight)
|
|
/ (mInitialTrackHeight + mInitialUpperTrackHeight);
|
|
|
|
int newTrackHeight = static_cast < int >
|
|
(mInitialTrackHeight + delta * proportion);
|
|
|
|
int newUpperTrackHeight = static_cast < int >
|
|
(mInitialUpperTrackHeight + delta * (1.0 - proportion));
|
|
|
|
//make sure neither track is smaller than its minimum height
|
|
if (newTrackHeight < mCapturedTrack->GetMinimizedHeight())
|
|
newTrackHeight = mCapturedTrack->GetMinimizedHeight();
|
|
if (newUpperTrackHeight < prev->GetMinimizedHeight())
|
|
newUpperTrackHeight = prev->GetMinimizedHeight();
|
|
|
|
mCapturedTrack->SetHeight(newTrackHeight);
|
|
prev->SetHeight(newUpperTrackHeight);
|
|
break;
|
|
}
|
|
case IsResizingBetweenLinkedTracks:
|
|
{
|
|
Track *next = mTracks->GetNext(mCapturedTrack);
|
|
int newUpperTrackHeight = mInitialUpperTrackHeight + delta;
|
|
int newTrackHeight = mInitialTrackHeight - delta;
|
|
|
|
// make sure neither track is smaller than its minimum height
|
|
if (newTrackHeight < next->GetMinimizedHeight()) {
|
|
newTrackHeight = next->GetMinimizedHeight();
|
|
newUpperTrackHeight =
|
|
mInitialUpperTrackHeight + mInitialTrackHeight - next->GetMinimizedHeight();
|
|
}
|
|
if (newUpperTrackHeight < mCapturedTrack->GetMinimizedHeight()) {
|
|
newUpperTrackHeight = mCapturedTrack->GetMinimizedHeight();
|
|
newTrackHeight =
|
|
mInitialUpperTrackHeight + mInitialTrackHeight - mCapturedTrack->GetMinimizedHeight();
|
|
}
|
|
|
|
mCapturedTrack->SetHeight(newUpperTrackHeight);
|
|
next->SetHeight(newTrackHeight);
|
|
break;
|
|
}
|
|
case IsResizing:
|
|
{
|
|
int newTrackHeight = mInitialTrackHeight + delta;
|
|
if (newTrackHeight < mCapturedTrack->GetMinimizedHeight())
|
|
newTrackHeight = mCapturedTrack->GetMinimizedHeight();
|
|
mCapturedTrack->SetHeight(newTrackHeight);
|
|
break;
|
|
}
|
|
default:
|
|
// don't refresh in this case.
|
|
return;
|
|
}
|
|
#endif // EXPERIMENTAL_OUTPUT_DISPLAY
|
|
Refresh(false);
|
|
}
|
|
|
|
/// HandleResize gets called when:
|
|
/// - A mouse-down event occurs in the "resize region" of a track,
|
|
/// i.e. to change its vertical height.
|
|
/// - A mouse event occurs and mIsResizing==true (i.e. while
|
|
/// the resize is going on)
|
|
void TrackPanel::HandleResize(wxMouseEvent & event)
|
|
{
|
|
if (event.LeftDown()) {
|
|
HandleResizeClick( event );
|
|
}
|
|
else if (event.LeftUp())
|
|
{
|
|
HandleResizeButtonUp( event );
|
|
}
|
|
else if (event.Dragging()) {
|
|
HandleResizeDrag( event );
|
|
}
|
|
}
|
|
|
|
/// Handle mouse wheel rotation (for zoom in/out, vertical and horizontal scrolling)
|
|
void TrackPanel::HandleWheelRotation(wxMouseEvent & event)
|
|
{
|
|
if (GetTracks()->IsEmpty())
|
|
// Scrolling and Zoom in and out commands are disabled when there are no tracks.
|
|
// This should be disabled too for consistency. Otherwise
|
|
// you do see changes in the time ruler.
|
|
return;
|
|
|
|
double steps = event.m_wheelRotation /
|
|
(event.m_wheelDelta > 0 ? (double)event.m_wheelDelta : 120.0);
|
|
|
|
if (event.ShiftDown()
|
|
#ifdef EXPERIMENTAL_SCRUBBING_SMOOTH_SCROLL
|
|
// Don't pan during smooth scrolling. That would conflict with keeping
|
|
// the play indicator centered.
|
|
&& !mSmoothScrollingScrub
|
|
#endif
|
|
)
|
|
{
|
|
// MM: Scroll left/right when used with Shift key down
|
|
mListener->TP_ScrollWindow(
|
|
mViewInfo->OffsetTimeByPixels(
|
|
mViewInfo->PositionToTime(0), 50.0 * -steps));
|
|
}
|
|
else if (event.CmdDown())
|
|
{
|
|
#if 0
|
|
// JKC: Alternative scroll wheel zooming code
|
|
// using AudacityProject zooming, which is smarter,
|
|
// it keeps selections on screen and centred if it can,
|
|
// also this ensures mousewheel and zoom buttons give same result.
|
|
double ZoomFactor = pow(2.0, steps);
|
|
AudacityProject *p = GetProject();
|
|
if( steps > 0 )
|
|
p->ZoomInByFactor( ZoomFactor );
|
|
else
|
|
p->ZoomOutByFactor( ZoomFactor );
|
|
#endif
|
|
// MM: Zoom in/out when used with Control key down
|
|
// We're converting pixel positions to times,
|
|
// counting pixels from the left edge of the track.
|
|
int trackLeftEdge = GetLeftOffset();
|
|
|
|
// Time corresponding to mouse position
|
|
wxCoord xx;
|
|
double center_h;
|
|
#ifdef EXPERIMENTAL_SCRUBBING_SMOOTH_SCROLL
|
|
if (mSmoothScrollingScrub) {
|
|
// Expand or contract about the center, ignoring mouse position
|
|
center_h = mViewInfo->h + mViewInfo->screen / 2.0;
|
|
xx = mViewInfo->TimeToPosition(center_h, trackLeftEdge);
|
|
}
|
|
else
|
|
#endif
|
|
{
|
|
xx = event.m_x;
|
|
center_h = mViewInfo->PositionToTime(xx, trackLeftEdge);
|
|
}
|
|
// Time corresponding to last (most far right) audio.
|
|
double audioEndTime = mTracks->GetEndTime();
|
|
|
|
// When zooming in in empty space, it's easy to 'lose' the waveform.
|
|
// This prevents it.
|
|
// IF zooming in
|
|
if (steps > 0)
|
|
{
|
|
// IF mouse is to right of audio
|
|
if (center_h > audioEndTime)
|
|
// Zooming brings far right of audio to mouse.
|
|
center_h = audioEndTime;
|
|
}
|
|
|
|
mViewInfo->ZoomBy(pow(2.0, steps));
|
|
|
|
double new_center_h = mViewInfo->PositionToTime(xx, trackLeftEdge);
|
|
mViewInfo->h += (center_h - new_center_h);
|
|
|
|
MakeParentRedrawScrollbars();
|
|
Refresh(false);
|
|
}
|
|
else
|
|
{
|
|
#ifdef EXPERIMENTAL_SCRUBBING_SCROLL_WHEEL
|
|
if (IsScrubbing()) {
|
|
const int newLogMaxScrubSpeed = mLogMaxScrubSpeed + steps;
|
|
static const double maxScrubSpeedBase =
|
|
pow(2.0, 1.0 / ScrubSpeedStepsPerOctave);
|
|
double newSpeed = pow(maxScrubSpeedBase, newLogMaxScrubSpeed);
|
|
if (newSpeed >= AudioIO::GetMinScrubSpeed() &&
|
|
newSpeed <= AudioIO::GetMaxScrubSpeed()) {
|
|
mLogMaxScrubSpeed = newLogMaxScrubSpeed;
|
|
mMaxScrubSpeed = newSpeed;
|
|
#ifdef EXPERIMENTAL_SCRUBBING_SMOOTH_SCROLL
|
|
if (!mSmoothScrollingScrub)
|
|
#endif
|
|
// Show the speed for one second
|
|
mScrubSpeedDisplayCountdown = kOneSecondCountdown + 1;
|
|
}
|
|
}
|
|
else
|
|
#endif
|
|
{
|
|
// MM: Scroll up/down when used without modifier keys
|
|
double lines = steps * 4 + mVertScrollRemainder;
|
|
mVertScrollRemainder = lines - floor(lines);
|
|
lines = floor(lines);
|
|
mListener->TP_ScrollUpDown((int)-lines);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Filter captured keys typed into LabelTracks.
|
|
void TrackPanel::OnCaptureKey(wxCommandEvent & event)
|
|
{
|
|
// Only deal with LabelTracks
|
|
Track *t = GetFocusedTrack();
|
|
if (!t || t->GetKind() != Track::Label) {
|
|
event.Skip();
|
|
return;
|
|
}
|
|
wxKeyEvent *kevent = (wxKeyEvent *)event.GetEventObject();
|
|
|
|
event.Skip(!((LabelTrack *)t)->CaptureKey(*kevent));
|
|
}
|
|
|
|
/// Allow typing into LabelTracks.
|
|
void TrackPanel::OnKeyDown(wxKeyEvent & event)
|
|
{
|
|
Track *t = GetFocusedTrack();
|
|
|
|
// Only deal with LabelTracks
|
|
if (!t || t->GetKind() != Track::Label) {
|
|
event.Skip();
|
|
return;
|
|
}
|
|
|
|
LabelTrack *lt = (LabelTrack *)t;
|
|
double bkpSel0 = mViewInfo->selectedRegion.t0(),
|
|
bkpSel1 = mViewInfo->selectedRegion.t1();
|
|
|
|
// Pass keystroke to labeltrack's handler and add to history if any
|
|
// updates were done
|
|
if (lt->OnKeyDown(mViewInfo->selectedRegion, event))
|
|
MakeParentPushState(_("Modified Label"),
|
|
_("Label Edit"),
|
|
PUSH_CONSOLIDATE);
|
|
|
|
// Make sure caret is in view
|
|
int x;
|
|
if (lt->CalcCursorX(this, &x)) {
|
|
ScrollIntoView(x);
|
|
}
|
|
|
|
// If selection modified, refresh
|
|
// Otherwise, refresh track display if the keystroke was handled
|
|
if( bkpSel0 != mViewInfo->selectedRegion.t0() ||
|
|
bkpSel1 != mViewInfo->selectedRegion.t1() )
|
|
Refresh( false );
|
|
else if (!event.GetSkipped())
|
|
RefreshTrack(t);
|
|
}
|
|
|
|
/// Allow typing into LabelTracks.
|
|
void TrackPanel::OnChar(wxKeyEvent & event)
|
|
{
|
|
// Only deal with LabelTracks
|
|
Track *t = GetFocusedTrack();
|
|
if (!t || t->GetKind() != Track::Label) {
|
|
event.Skip();
|
|
return;
|
|
}
|
|
|
|
double bkpSel0 = mViewInfo->selectedRegion.t0(),
|
|
bkpSel1 = mViewInfo->selectedRegion.t1();
|
|
// Pass keystroke to labeltrack's handler and add to history if any
|
|
// updates were done
|
|
if (((LabelTrack *)t)->OnChar(mViewInfo->selectedRegion, event))
|
|
MakeParentPushState(_("Modified Label"),
|
|
_("Label Edit"),
|
|
PUSH_CONSOLIDATE);
|
|
|
|
// If selection modified, refresh
|
|
// Otherwise, refresh track display if the keystroke was handled
|
|
if( bkpSel0 != mViewInfo->selectedRegion.t0() ||
|
|
bkpSel1 != mViewInfo->selectedRegion.t1() )
|
|
Refresh( false );
|
|
else if (!event.GetSkipped())
|
|
RefreshTrack(t);
|
|
}
|
|
|
|
/// Should handle the case when the mouse capture is lost.
|
|
void TrackPanel::OnCaptureLost(wxMouseCaptureLostEvent & WXUNUSED(event))
|
|
{
|
|
wxMouseEvent e(wxEVT_LEFT_UP);
|
|
|
|
e.m_x = mMouseMostRecentX;
|
|
e.m_y = mMouseMostRecentY;
|
|
|
|
OnMouseEvent(e);
|
|
}
|
|
|
|
/// This handles just generic mouse events. Then, based
|
|
/// on our current state, we forward the mouse events to
|
|
/// various interested parties.
|
|
void TrackPanel::OnMouseEvent(wxMouseEvent & event)
|
|
{
|
|
if (event.m_wheelRotation != 0)
|
|
HandleWheelRotation(event);
|
|
|
|
if (!mAutoScrolling) {
|
|
mMouseMostRecentX = event.m_x;
|
|
mMouseMostRecentY = event.m_y;
|
|
}
|
|
|
|
if (event.LeftDown()) {
|
|
mCapturedTrack = NULL;
|
|
|
|
// The activate event is used to make the
|
|
// parent window 'come alive' if it didn't have focus.
|
|
wxActivateEvent e;
|
|
GetParent()->GetEventHandler()->ProcessEvent(e);
|
|
|
|
// wxTimers seem to be a little unreliable, so this
|
|
// "primes" it to make sure it keeps going for a while...
|
|
|
|
// When this timer fires, we call TrackPanel::OnTimer and
|
|
// possibly update the screen for offscreen scrolling.
|
|
mTimer.Stop();
|
|
mTimer.Start(kTimerInterval, FALSE);
|
|
}
|
|
|
|
if (event.ButtonDown()) {
|
|
SetFocus();
|
|
}
|
|
if (event.ButtonUp()) {
|
|
if (HasCapture())
|
|
ReleaseMouse();
|
|
}
|
|
|
|
if (event.Leaving() && !event.ButtonIsDown(wxMOUSE_BTN_ANY))
|
|
{
|
|
if (mMouseCapture != IsPanSliding && mMouseCapture != IsGainSliding)
|
|
{
|
|
SetCapturedTrack(NULL);
|
|
#if defined(__WXMAC__)
|
|
// We must install the cursor ourselves since the window under
|
|
// the mouse is no longer this one and wx2.8.12 makes that check.
|
|
// Should re-evaluate with wx3.
|
|
wxSTANDARD_CURSOR->MacInstall();
|
|
#endif
|
|
}
|
|
}
|
|
|
|
switch( mMouseCapture )
|
|
{
|
|
case IsVZooming:
|
|
HandleVZoom(event);
|
|
break;
|
|
case IsClosing:
|
|
HandleClosing(event);
|
|
break;
|
|
case IsPopping:
|
|
HandlePopping(event);
|
|
break;
|
|
case IsMuting:
|
|
HandleMutingSoloing(event, false);
|
|
break;
|
|
case IsSoloing:
|
|
HandleMutingSoloing(event, true);
|
|
break;
|
|
case IsResizing:
|
|
case IsResizingBetweenLinkedTracks:
|
|
case IsResizingBelowLinkedTracks:
|
|
HandleResize(event);
|
|
HandleCursor(event);
|
|
break;
|
|
case IsRearranging:
|
|
HandleRearrange(event);
|
|
break;
|
|
case IsGainSliding:
|
|
HandleSliders(event, false);
|
|
break;
|
|
case IsPanSliding:
|
|
HandleSliders(event, true);
|
|
break;
|
|
case IsMinimizing:
|
|
HandleMinimizing(event);
|
|
break;
|
|
case IsZooming:
|
|
HandleZoom(event);
|
|
break;
|
|
case IsAdjustingLabel:
|
|
HandleLabelTrackMouseEvent((LabelTrack *)mCapturedTrack, mCapturedRect, event);
|
|
break;
|
|
default: //includes case of IsUncaptured
|
|
// This is where most button-downs are detected
|
|
HandleTrackSpecificMouseEvent(event);
|
|
break;
|
|
}
|
|
|
|
if (event.ButtonDown() && IsMouseCaptured()) {
|
|
if (!HasCapture())
|
|
CaptureMouse();
|
|
}
|
|
|
|
//EnsureVisible should be called after the up-click.
|
|
if (event.ButtonUp()) {
|
|
wxRect r;
|
|
|
|
Track *t = FindTrack(event.m_x, event.m_y, false, false, &r);
|
|
if (t)
|
|
EnsureVisible(t);
|
|
}
|
|
}
|
|
|
|
bool TrackPanel::HandleTrackLocationMouseEvent(WaveTrack * track, wxRect &rect, wxMouseEvent &event)
|
|
{
|
|
// FIXME: Disable this and return true when CutLines aren't showing?
|
|
// (Don't use gPrefs-> for the fix as registry access is slow).
|
|
|
|
if (mMouseCapture == WasOverCutLine)
|
|
{
|
|
if (event.ButtonUp()) {
|
|
mMouseCapture = IsUncaptured;
|
|
return false;
|
|
}
|
|
else
|
|
// Needed to avoid select events after button down
|
|
return true;
|
|
}
|
|
else if (!IsUnsafe() && IsOverCutline(track, rect, event))
|
|
{
|
|
if (!mCapturedTrackLocationRect.Contains(event.m_x, event.m_y))
|
|
{
|
|
SetCapturedTrack( NULL );
|
|
return false;
|
|
}
|
|
|
|
bool handled = false;
|
|
|
|
if (event.LeftDown())
|
|
{
|
|
if (mCapturedTrackLocation.typ == WaveTrackLocation::locationCutLine)
|
|
{
|
|
// When user presses left button on cut line, expand the line again
|
|
double cutlineStart = 0, cutlineEnd = 0;
|
|
|
|
if (track->ExpandCutLine(mCapturedTrackLocation.pos, &cutlineStart, &cutlineEnd))
|
|
{
|
|
WaveTrack* linked = (WaveTrack*)mTracks->GetLink(track);
|
|
if (linked &&
|
|
!linked->ExpandCutLine(mCapturedTrackLocation.pos))
|
|
return false;
|
|
|
|
mViewInfo->selectedRegion.setTimes(cutlineStart, cutlineEnd);
|
|
DisplaySelection();
|
|
MakeParentPushState(_("Expanded Cut Line"), _("Expand"));
|
|
handled = true;
|
|
}
|
|
}
|
|
else if (mCapturedTrackLocation.typ == WaveTrackLocation::locationMergePoint) {
|
|
if (!track->MergeClips(mCapturedTrackLocation.clipidx1, mCapturedTrackLocation.clipidx2))
|
|
return false;
|
|
|
|
WaveTrack* linked = (WaveTrack*)mTracks->GetLink(track);
|
|
if (linked &&
|
|
!linked->MergeClips(mCapturedTrackLocation.clipidx1, mCapturedTrackLocation.clipidx2))
|
|
return false;
|
|
|
|
MakeParentPushState(_("Merged Clips"),_("Merge"), PUSH_CONSOLIDATE);
|
|
handled = true;
|
|
}
|
|
}
|
|
|
|
if (!handled && event.RightDown())
|
|
{
|
|
track->RemoveCutLine(mCapturedTrackLocation.pos);
|
|
WaveTrack* linked = (WaveTrack*)mTracks->GetLink(track);
|
|
if (linked)
|
|
linked->RemoveCutLine(mCapturedTrackLocation.pos);
|
|
MakeParentPushState(_("Removed Cut Line"), _("Remove") );
|
|
handled = true;
|
|
}
|
|
|
|
if (handled)
|
|
{
|
|
SetCapturedTrack( NULL );
|
|
// Effect happened at button-down, but treat like a dragging mode until
|
|
// button-up.
|
|
mMouseCapture = WasOverCutLine;
|
|
RefreshTrack(track);
|
|
return true;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool TrackPanel::IsOverCutline(WaveTrack * track, wxRect &rect, wxMouseEvent &event)
|
|
{
|
|
for (int i=0; i<track->GetNumCachedLocations(); i++)
|
|
{
|
|
WaveTrack::Location loc = track->GetCachedLocation(i);
|
|
|
|
const double x = mViewInfo->TimeToPosition(loc.pos);
|
|
if (x >= 0 && x < rect.width)
|
|
{
|
|
wxRect locRect;
|
|
locRect.x = int(rect.x + x) - 5;
|
|
locRect.width = 11;
|
|
locRect.y = rect.y;
|
|
locRect.height = rect.height;
|
|
if (locRect.Contains(event.m_x, event.m_y))
|
|
{
|
|
mCapturedTrackLocation = loc;
|
|
mCapturedTrackLocationRect = locRect;
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
/// Event has happened on a track and it has been determined to be a label track.
|
|
bool TrackPanel::HandleLabelTrackMouseEvent(LabelTrack * lTrack, wxRect &r, wxMouseEvent & event)
|
|
{
|
|
/// \todo This method is one of a large number of methods in
|
|
/// TrackPanel which suitably modified belong in other classes.
|
|
if(event.LeftDown())
|
|
{
|
|
TrackListIterator iter(mTracks);
|
|
Track *n = iter.First();
|
|
|
|
while (n) {
|
|
if (n->GetKind() == Track::Label && lTrack != n) {
|
|
((LabelTrack *)n)->ResetFlags();
|
|
((LabelTrack *)n)->Unselect();
|
|
}
|
|
n = iter.Next();
|
|
}
|
|
|
|
//If the button was pressed, check to see if we are over
|
|
//a glyph (this is the second of three calls to the method).
|
|
//std::cout << ((LabelTrack*)pTrack)->OverGlyph(event.m_x, event.m_y) << std::endl;
|
|
if(lTrack->OverGlyph(event.m_x, event.m_y))
|
|
{
|
|
SetCapturedTrack(lTrack, IsAdjustingLabel);
|
|
mCapturedRect = r;
|
|
mCapturedRect.x += kLeftInset;
|
|
mCapturedRect.width -= kLeftInset;
|
|
}
|
|
} else if (event.Dragging()) {
|
|
;
|
|
} else if (event.LeftUp() && mCapturedTrack && (mCapturedTrack->GetKind() == Track::Label)) {
|
|
SetCapturedTrack( NULL );
|
|
}
|
|
|
|
if (lTrack->HandleMouse(event, mCapturedRect,
|
|
*mViewInfo, &mViewInfo->selectedRegion)) {
|
|
|
|
MakeParentPushState(_("Modified Label"),
|
|
_("Label Edit"),
|
|
PUSH_CONSOLIDATE);
|
|
}
|
|
|
|
|
|
if (event.RightUp()) {
|
|
// popup menu for editing
|
|
RefreshTrack(lTrack);
|
|
|
|
if ((lTrack->getSelectedIndex() != -1) && lTrack->OverTextBox(lTrack->GetLabel(lTrack->getSelectedIndex()), event.m_x, event.m_y)) {
|
|
mPopupMenuTarget = lTrack;
|
|
mLabelTrackInfoMenu->Enable(OnCutSelectedTextID, lTrack->IsTextSelected());
|
|
mLabelTrackInfoMenu->Enable(OnCopySelectedTextID, lTrack->IsTextSelected());
|
|
mLabelTrackInfoMenu->Enable(OnPasteSelectedTextID, lTrack->IsTextClipSupported());
|
|
mLabelTrackInfoMenu->Enable(OnDeleteSelectedLabelID, true);
|
|
PopupMenu(mLabelTrackInfoMenu, event.m_x + 1, event.m_y + 1);
|
|
mPopupMenuTarget = NULL;
|
|
// it's an invalid dragging event
|
|
lTrack->SetWrongDragging(true);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
//If we are adjusting a label on a labeltrack, do not do anything
|
|
//that follows. Instead, redraw the track.
|
|
if(mMouseCapture == IsAdjustingLabel)
|
|
{
|
|
RefreshTrack(lTrack);
|
|
return true;
|
|
}
|
|
|
|
// handle dragging
|
|
if(event.Dragging()) {
|
|
// locate the initial mouse position
|
|
if (event.LeftIsDown()) {
|
|
if (mLabelTrackStartXPos == -1) {
|
|
mLabelTrackStartXPos = event.m_x;
|
|
mLabelTrackStartYPos = event.m_y;
|
|
|
|
if ((lTrack->getSelectedIndex() != -1) &&
|
|
lTrack->OverTextBox(
|
|
lTrack->GetLabel(lTrack->getSelectedIndex()),
|
|
mLabelTrackStartXPos,
|
|
mLabelTrackStartYPos))
|
|
{
|
|
mLabelTrackStartYPos = -1;
|
|
}
|
|
}
|
|
// if initial mouse position in the text box
|
|
// then only drag text
|
|
if (mLabelTrackStartYPos == -1) {
|
|
RefreshTrack(lTrack);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// handle mouse left button up
|
|
if (event.LeftUp()) {
|
|
mLabelTrackStartXPos = -1;
|
|
}
|
|
|
|
// handle shift+ctrl down
|
|
/*if (event.ShiftDown()) { // && event.ControlDown()) {
|
|
lTrack->SetHighlightedByKey(true);
|
|
Refresh(false);
|
|
return;
|
|
}*/
|
|
|
|
// handle shift+mouse left button
|
|
if (event.ShiftDown() && event.ButtonDown() && (lTrack->getSelectedIndex() != -1)) {
|
|
// if the mouse is clicked in text box, set flags
|
|
if (lTrack->OverTextBox(lTrack->GetLabel(lTrack->getSelectedIndex()), event.m_x, event.m_y)) {
|
|
lTrack->SetInBox(true);
|
|
lTrack->SetDragXPos(event.m_x);
|
|
lTrack->SetResetCursorPos(true);
|
|
RefreshTrack(lTrack);
|
|
return true;
|
|
}
|
|
}
|
|
// return false, there is more to do...
|
|
return false;
|
|
}
|
|
|
|
// AS: I don't really understand why this code is sectioned off
|
|
// from the other OnMouseEvent code.
|
|
void TrackPanel::HandleTrackSpecificMouseEvent(wxMouseEvent & event)
|
|
{
|
|
Track * pTrack;
|
|
wxRect rTrack;
|
|
wxRect rLabel;
|
|
|
|
bool unsafe = IsUnsafe();
|
|
|
|
FindTrack(event.m_x, event.m_y, true, true, &rLabel);
|
|
pTrack = FindTrack(event.m_x, event.m_y, false, false, &rTrack);
|
|
|
|
//call HandleResize if I'm over the border area
|
|
if (event.LeftDown() &&
|
|
(within(event.m_y, rTrack.y + rTrack.height, TRACK_RESIZE_REGION)
|
|
|| within(event.m_y, rLabel.y + rLabel.height,
|
|
TRACK_RESIZE_REGION))) {
|
|
HandleResize(event);
|
|
HandleCursor(event);
|
|
return;
|
|
}
|
|
|
|
//Determine if user clicked on the track's left-hand label
|
|
if (!mCapturedTrack && event.m_x < GetLeftOffset()) {
|
|
if (event.m_x >= GetVRulerOffset()) {
|
|
if( !event.Dragging() ) // JKC: Only want the mouse down event.
|
|
HandleVZoom(event);
|
|
HandleCursor(event);
|
|
}
|
|
else {
|
|
HandleLabelClick(event);
|
|
HandleCursor(event);
|
|
}
|
|
return;
|
|
}
|
|
|
|
//Determine if user clicked on a label track.
|
|
//If so, use MouseDown handler for the label track.
|
|
if (pTrack && (pTrack->GetKind() == Track::Label))
|
|
{
|
|
if (HandleLabelTrackMouseEvent((LabelTrack *)pTrack, rTrack, event))
|
|
return;
|
|
}
|
|
|
|
#ifdef EXPERIMENTAL_SCRUBBING_BASIC
|
|
if ((!pTrack ||
|
|
pTrack->GetKind() == Track::Wave) &&
|
|
IsScrubbing()) {
|
|
if (event.LeftDown()) {
|
|
mScrubSeekPress = true;
|
|
return;
|
|
}
|
|
else if (event.LeftIsDown())
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
bool handled = false;
|
|
|
|
if (pTrack && (pTrack->GetKind() == Track::Wave) &&
|
|
(mMouseCapture == IsUncaptured || mMouseCapture == WasOverCutLine))
|
|
handled = HandleTrackLocationMouseEvent((WaveTrack *)pTrack, rTrack, event);
|
|
|
|
ToolsToolBar * pTtb = mListener->TP_GetToolsToolBar();
|
|
if( !handled && pTtb != NULL )
|
|
{
|
|
int toolToUse = DetermineToolToUse(pTtb, event);
|
|
|
|
switch (toolToUse) {
|
|
case selectTool:
|
|
HandleSelect(event);
|
|
break;
|
|
case envelopeTool:
|
|
if (!unsafe)
|
|
HandleEnvelope(event);
|
|
break;
|
|
case slideTool:
|
|
if (!unsafe)
|
|
HandleSlide(event);
|
|
break;
|
|
case zoomTool:
|
|
HandleZoom(event);
|
|
break;
|
|
case drawTool:
|
|
if (!unsafe)
|
|
HandleSampleEditing(event);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ((event.Moving() || event.LeftUp()) &&
|
|
(mMouseCapture == IsUncaptured ))
|
|
// (mMouseCapture != IsSelecting ) &&
|
|
// (mMouseCapture != IsEnveloping) &&
|
|
// (mMouseCapture != IsSliding) )
|
|
{
|
|
HandleCursor(event);
|
|
}
|
|
if (event.LeftUp()) {
|
|
mCapturedTrack = NULL;
|
|
}
|
|
}
|
|
|
|
/// If we are in multimode, looks at the type of track and where we are on it to
|
|
/// determine what object we are hovering over and hence what tool to use.
|
|
/// @param pTtb - A pointer to the tools tool bar
|
|
/// @param event - Mouse event, with info about position and what mouse buttons are down.
|
|
int TrackPanel::DetermineToolToUse( ToolsToolBar * pTtb, wxMouseEvent & event)
|
|
{
|
|
int currentTool = pTtb->GetCurrentTool();
|
|
|
|
// Unless in Multimode keep using the current tool.
|
|
if( !pTtb->IsDown(multiTool) )
|
|
return currentTool;
|
|
|
|
// We NEVER change tools whilst we are dragging.
|
|
if( event.Dragging() || event.LeftUp() )
|
|
return currentTool;
|
|
|
|
// Just like dragging.
|
|
// But, this event might be the final button up
|
|
// so keep the same tool.
|
|
// if( mIsSliding || mIsSelecting || mIsEnveloping )
|
|
if( mMouseCapture != IsUncaptured )
|
|
return currentTool;
|
|
|
|
// So now we have to find out what we are near to..
|
|
wxRect r;
|
|
|
|
Track *pTrack = FindTrack(event.m_x, event.m_y, false, false, &r);
|
|
if( !pTrack )
|
|
return currentTool;
|
|
|
|
int trackKind = pTrack->GetKind();
|
|
currentTool = selectTool; // the default.
|
|
|
|
if (event.ButtonIsDown(wxMOUSE_BTN_RIGHT) || event.RightUp()){
|
|
currentTool = zoomTool;
|
|
}
|
|
else if (trackKind == Track::Time){
|
|
currentTool = envelopeTool;
|
|
} else if( trackKind == Track::Label ){
|
|
currentTool = selectTool;
|
|
} else if( trackKind != Track::Wave) {
|
|
currentTool = selectTool;
|
|
// So we are in a wave track.
|
|
//FIXME: Not necessarily. Haven't checked Track::Note (#if defined(USE_MIDI)).
|
|
// From here on the order in which we hit test determines
|
|
// which tool takes priority in the rare cases where it
|
|
// could be more than one.
|
|
} else if (event.CmdDown()){
|
|
// msmeyer: If control is down, slide single clip
|
|
// msmeyer: If control and shift are down, slide all clips
|
|
currentTool = slideTool;
|
|
} else if( HitTestEnvelope( pTrack, r, event ) ){
|
|
currentTool = envelopeTool;
|
|
} else if( HitTestSlide( pTrack, r, event )){
|
|
currentTool = slideTool;
|
|
} else if( HitTestSamples( pTrack, r, event )){
|
|
currentTool = drawTool;
|
|
}
|
|
|
|
//Use the false argument since in multimode we don't
|
|
//want the button indicating which tool is in use to be updated.
|
|
pTtb->SetCurrentTool( currentTool, false );
|
|
return currentTool;
|
|
}
|
|
|
|
|
|
#ifdef USE_MIDI
|
|
bool TrackPanel::HitTestStretch(Track *track, wxRect &r, wxMouseEvent & event)
|
|
{
|
|
// later, we may want a different policy, but for now, stretch is
|
|
// selected when the cursor is near the center of the track and
|
|
// within the selection
|
|
if (!track || !track->GetSelected() || track->GetKind() != Track::Note ||
|
|
IsUnsafe()) {
|
|
return false;
|
|
}
|
|
int center = r.y + r.height / 2;
|
|
int distance = abs(event.m_y - center);
|
|
const int yTolerance = 10;
|
|
wxInt64 leftSel = mViewInfo->TimeToPosition(mViewInfo->selectedRegion.t0(), r.x);
|
|
wxInt64 rightSel = mViewInfo->TimeToPosition(mViewInfo->selectedRegion.t1(), r.x);
|
|
// Something is wrong if right edge comes before left edge
|
|
wxASSERT(!(rightSel < leftSel));
|
|
return (leftSel <= event.m_x && event.m_x <= rightSel &&
|
|
distance < yTolerance);
|
|
}
|
|
#endif
|
|
|
|
|
|
/// method that tells us if the mouse event landed on an
|
|
/// envelope boundary.
|
|
bool TrackPanel::HitTestEnvelope(Track *track, wxRect &r, wxMouseEvent & event)
|
|
{
|
|
wxASSERT(track);
|
|
if( track->GetKind() != Track::Wave )
|
|
return false;
|
|
WaveTrack *wavetrack = (WaveTrack *)track;
|
|
Envelope *envelope = wavetrack->GetEnvelopeAtX(event.GetX());
|
|
|
|
if (!envelope)
|
|
return false;
|
|
|
|
const int displayType = wavetrack->GetDisplay();
|
|
// Not an envelope hit, unless we're using a type of wavetrack display
|
|
// suitable for envelopes operations, ie one of the Wave displays.
|
|
if ( displayType != WaveTrack::Waveform)
|
|
return false; // No envelope, not a hit, so return.
|
|
|
|
// Get envelope point, range 0.0 to 1.0
|
|
const bool dB = !wavetrack->GetWaveformSettings().isLinear();
|
|
// Convert x to time.
|
|
const double envValue = envelope->GetValue(mViewInfo->PositionToTime(event.m_x, r.x));
|
|
|
|
float zoomMin, zoomMax;
|
|
wavetrack->GetDisplayBounds(&zoomMin, &zoomMax);
|
|
|
|
// Get y position of envelope point.
|
|
int yValue = GetWaveYPos( envValue,
|
|
zoomMin, zoomMax,
|
|
r.height, dB, true, mViewInfo->dBr, false) + r.y;
|
|
|
|
// Get y position of center line
|
|
int ctr = GetWaveYPos( 0.0,
|
|
zoomMin, zoomMax,
|
|
r.height, dB, true, mViewInfo->dBr, false) + r.y;
|
|
|
|
// Get y distance of mouse from center line (in pixels).
|
|
int yMouse = abs(ctr - event.m_y);
|
|
// Get y distance of envelope from center line (in pixels)
|
|
yValue = abs(ctr-yValue);
|
|
|
|
// JKC: It happens that the envelope is actually drawn offset from its
|
|
// 'true' position (it is 3 pixels wide). yMisalign is really a fudge
|
|
// factor to allow us to hit it exactly, but I wouldn't dream of
|
|
// calling it yFudgeFactor :)
|
|
const int yMisalign = 2;
|
|
// Perhaps yTolerance should be put into preferences?
|
|
const int yTolerance = 5; // how far from envelope we may be and count as a hit.
|
|
int distance;
|
|
|
|
// For amplification using the envelope we introduced the idea of contours.
|
|
// The contours have the same shape as the envelope, which may be partially off-screen.
|
|
// The contours are closer in to the center line.
|
|
int ContourSpacing = (int) (r.height / (2* (zoomMax-zoomMin)));
|
|
const int MaxContours = 2;
|
|
|
|
// Adding ContourSpacing/2 selects a region either side of the contour.
|
|
int yDisplace = yValue - yMisalign - yMouse + ContourSpacing/2;
|
|
if (yDisplace > (MaxContours * ContourSpacing))
|
|
return false;
|
|
// Subtracting the ContourSpacing/2 we added earlier ensures distance is centred on the contour.
|
|
distance = abs( ( yDisplace % ContourSpacing ) - ContourSpacing/2);
|
|
return( distance < yTolerance );
|
|
}
|
|
|
|
/// method that tells us if the mouse event landed on an
|
|
/// editable sample
|
|
bool TrackPanel::HitTestSamples(Track *track, wxRect &r, wxMouseEvent & event)
|
|
{
|
|
wxASSERT(track);
|
|
if( track->GetKind() != Track::Wave )
|
|
return false;
|
|
|
|
WaveTrack *wavetrack = (WaveTrack *)track;
|
|
//Get rate in order to calculate the critical zoom threshold
|
|
double rate = wavetrack->GetRate();
|
|
|
|
const int displayType = wavetrack->GetDisplay();
|
|
if (WaveTrack::Waveform != displayType)
|
|
return false; // Not a wave, so return.
|
|
const bool dB = !wavetrack->GetWaveformSettings().isLinear();
|
|
|
|
const double tt = mViewInfo->PositionToTime(event.m_x, r.x);
|
|
if (!SampleResolutionTest(*mViewInfo, wavetrack, tt, rate))
|
|
return false;
|
|
|
|
// Just get one sample.
|
|
float oneSample;
|
|
sampleCount s0 = (sampleCount)(tt * rate + 0.5);
|
|
wavetrack->Get((samplePtr)&oneSample, floatSample, s0, 1);
|
|
|
|
// Get y distance of envelope point from center line (in pixels).
|
|
float zoomMin, zoomMax;
|
|
|
|
wavetrack->GetDisplayBounds(&zoomMin, &zoomMax);
|
|
|
|
double envValue = 1.0;
|
|
Envelope* env = wavetrack->GetEnvelopeAtX(event.GetX());
|
|
if (env)
|
|
envValue = env->GetValue(tt);
|
|
|
|
int yValue = GetWaveYPos( oneSample * envValue,
|
|
zoomMin, zoomMax,
|
|
r.height, dB, true, mViewInfo->dBr, false) + r.y;
|
|
|
|
// Get y position of mouse (in pixels)
|
|
int yMouse = event.m_y;
|
|
|
|
// Perhaps yTolerance should be put into preferences?
|
|
const int yTolerance = 10; // More tolerance on samples than on envelope.
|
|
return( abs( yValue - yMouse ) < yTolerance );
|
|
}
|
|
|
|
/// method that tells us if the mouse event landed on a
|
|
/// time-slider that allows us to time shift the sequence.
|
|
bool TrackPanel::HitTestSlide(Track * WXUNUSED(track), wxRect &r, wxMouseEvent & event)
|
|
{
|
|
// Perhaps we should delegate this to TrackArtist as only TrackArtist
|
|
// knows what the real sizes are??
|
|
|
|
// The drag Handle width includes border, width and a little extra margin.
|
|
const int adjustedDragHandleWidth = 14;
|
|
// The hotspot for the cursor isn't at its centre. Adjust for this.
|
|
const int hotspotOffset = 5;
|
|
|
|
// We are doing an approximate test here - is the mouse in the right or left border?
|
|
if( event.m_x + hotspotOffset < r.x + adjustedDragHandleWidth)
|
|
return true;
|
|
|
|
if( event.m_x + hotspotOffset > r.x + r.width - adjustedDragHandleWidth)
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
double TrackPanel::GetMostRecentXPos()
|
|
{
|
|
return mViewInfo->PositionToTime(mMouseMostRecentX, GetLabelWidth());
|
|
}
|
|
|
|
void TrackPanel::RefreshTrack(Track *trk, bool refreshbacking)
|
|
{
|
|
Track *link = trk->GetLink();
|
|
|
|
if (link && !trk->GetLinked()) {
|
|
trk = link;
|
|
link = trk->GetLink();
|
|
}
|
|
|
|
wxRect r(kLeftInset,
|
|
-mViewInfo->vpos + trk->GetY() + kTopInset,
|
|
GetRect().GetWidth() - kLeftInset * 2 - 1,
|
|
trk->GetHeight() - kTopInset - 1);
|
|
|
|
if (link) {
|
|
r.height += link->GetHeight();
|
|
}
|
|
|
|
#ifdef EXPERIMENTAL_OUTPUT_DISPLAY
|
|
else if(MONO_WAVE_PAN(trk)){
|
|
r.height += trk->GetHeight(true);
|
|
}
|
|
#endif
|
|
|
|
if( refreshbacking )
|
|
{
|
|
mRefreshBacking = true;
|
|
}
|
|
|
|
Refresh( false, &r );
|
|
}
|
|
|
|
|
|
/// This method overrides Refresh() of wxWindow so that the
|
|
/// boolean play indictaor can be set to false, so that an old play indicator that is
|
|
/// no longer there won't get XORed (to erase it), thus redrawing it on the
|
|
/// TrackPanel
|
|
void TrackPanel::Refresh(bool eraseBackground /* = TRUE */,
|
|
const wxRect *rect /* = NULL */)
|
|
{
|
|
// Tell OnPaint() to refresh the backing bitmap.
|
|
//
|
|
// Originally I had the check within the OnPaint() routine and it
|
|
// was working fine. That was until I found that, even though a full
|
|
// refresh was requested, Windows only set the onscreen portion of a
|
|
// window as damaged.
|
|
//
|
|
// So, if any part of the trackpanel was off the screen, full refreshes
|
|
// didn't work and the display got corrupted.
|
|
if( !rect || ( *rect == GetRect() ) )
|
|
{
|
|
mRefreshBacking = true;
|
|
}
|
|
wxWindow::Refresh(eraseBackground, rect);
|
|
DisplaySelection();
|
|
}
|
|
|
|
/// Draw the actual track areas. We only draw the borders
|
|
/// and the little buttons and menues and whatnot here, the
|
|
/// actual contents of each track are drawn by the TrackArtist.
|
|
void TrackPanel::DrawTracks(wxDC * dc)
|
|
{
|
|
#if defined(__WXMAC__)
|
|
dc->GetGraphicsContext()->SetAntialiasMode(wxANTIALIAS_NONE);
|
|
#endif
|
|
wxRegion region = GetUpdateRegion();
|
|
|
|
wxRect clip = GetRect();
|
|
|
|
wxRect panelRect = clip;
|
|
panelRect.y = -mViewInfo->vpos;
|
|
|
|
wxRect tracksRect = panelRect;
|
|
tracksRect.x += GetLabelWidth();
|
|
tracksRect.width -= GetLabelWidth();
|
|
|
|
ToolsToolBar *pTtb = mListener->TP_GetToolsToolBar();
|
|
bool bMultiToolDown = pTtb->IsDown(multiTool);
|
|
bool envelopeFlag = pTtb->IsDown(envelopeTool) || bMultiToolDown;
|
|
bool bigPointsFlag = pTtb->IsDown(drawTool) || bMultiToolDown;
|
|
bool sliderFlag = bMultiToolDown;
|
|
|
|
// The track artist actually draws the stuff inside each track
|
|
mTrackArtist->DrawTracks(mTracks, GetProject()->GetFirstVisible(),
|
|
*dc, region, tracksRect, clip,
|
|
mViewInfo->selectedRegion, *mViewInfo,
|
|
envelopeFlag, bigPointsFlag, sliderFlag);
|
|
|
|
DrawEverythingElse(dc, region, clip);
|
|
}
|
|
|
|
/// Draws 'Everything else'. In particular it draws:
|
|
/// - Drop shadow for tracks and vertical rulers.
|
|
/// - Zooming Indicators.
|
|
/// - Fills in space below the tracks.
|
|
void TrackPanel::DrawEverythingElse(wxDC * dc,
|
|
const wxRegion ®ion,
|
|
const wxRect & clip)
|
|
{
|
|
// We draw everything else
|
|
|
|
wxRect focusRect(-1, -1, 0, 0);
|
|
wxRect trackRect = clip;
|
|
trackRect.height = 0; // for drawing background in no tracks case.
|
|
|
|
VisibleTrackIterator iter(GetProject());
|
|
for (Track *t = iter.First(); t; t = iter.Next()) {
|
|
trackRect.y = t->GetY() - mViewInfo->vpos;
|
|
trackRect.height = t->GetHeight();
|
|
|
|
// If this track is linked to the next one, display a common
|
|
// border for both, otherwise draw a normal border
|
|
wxRect r = trackRect;
|
|
bool skipBorder = false;
|
|
Track *l = t->GetLink();
|
|
|
|
if (t->GetLinked()) {
|
|
r.height += l->GetHeight();
|
|
}
|
|
else if (l && trackRect.y >= 0) {
|
|
skipBorder = true;
|
|
}
|
|
|
|
#ifdef EXPERIMENTAL_OUTPUT_DISPLAY
|
|
if(MONO_WAVE_PAN(t)){
|
|
r.height += t->GetHeight(true);
|
|
}
|
|
#endif
|
|
|
|
// If the previous track is linked to this one but isn't on the screen
|
|
// (and thus would have been skipped by VisibleTrackIterator) we need to
|
|
// draw that track's border instead.
|
|
Track *borderTrack = t;
|
|
wxRect borderRect = r, borderTrackRect = trackRect;
|
|
|
|
if (l && !t->GetLinked() && trackRect.y < 0)
|
|
{
|
|
borderTrack = l;
|
|
|
|
borderTrackRect.y = l->GetY() - mViewInfo->vpos;
|
|
borderTrackRect.height = l->GetHeight();
|
|
|
|
borderRect = borderTrackRect;
|
|
borderRect.height += t->GetHeight();
|
|
}
|
|
|
|
if (!skipBorder) {
|
|
if (mAx->IsFocused(t)) {
|
|
focusRect = borderRect;
|
|
}
|
|
DrawOutside(borderTrack, dc, borderRect, borderTrackRect);
|
|
}
|
|
|
|
// Believe it or not, we can speed up redrawing if we don't
|
|
// redraw the vertical ruler when only the waveform data has
|
|
// changed. An example is during recording.
|
|
|
|
#if DEBUG_DRAW_TIMING
|
|
// wxRect rbox = region.GetBox();
|
|
// wxPrintf(wxT("Update Region: %d %d %d %d\n"),
|
|
// rbox.x, rbox.y, rbox.width, rbox.height);
|
|
#endif
|
|
|
|
if (region.Contains(0, trackRect.y, GetLeftOffset(), trackRect.height)) {
|
|
wxRect r = trackRect;
|
|
r.x += GetVRulerOffset();
|
|
r.y += kTopInset;
|
|
r.width = GetVRulerWidth();
|
|
r.height -= (kTopInset + 2);
|
|
mTrackArtist->DrawVRuler(t, dc, r);
|
|
}
|
|
|
|
#ifdef EXPERIMENTAL_OUTPUT_DISPLAY
|
|
if(MONO_WAVE_PAN(t)){
|
|
trackRect.y = t->GetY(true) - mViewInfo->vpos;
|
|
trackRect.height = t->GetHeight(true);
|
|
if (region.Contains(0, trackRect.y, GetLeftOffset(), trackRect.height)) {
|
|
wxRect r = trackRect;
|
|
r.x += GetVRulerOffset();
|
|
r.y += kTopInset;
|
|
r.width = GetVRulerWidth();
|
|
r.height -= (kTopInset + 2);
|
|
mTrackArtist->DrawVRuler(t, dc, r);
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
if ((mMouseCapture == IsZooming || mMouseCapture == IsVZooming) &&
|
|
IsDragZooming()
|
|
// note track zooming now works like audio track
|
|
//#ifdef USE_MIDI
|
|
// && mCapturedTrack && mCapturedTrack->GetKind() != Track::Note
|
|
//#endif
|
|
) {
|
|
DrawZooming(dc, clip);
|
|
}
|
|
|
|
// Paint over the part below the tracks
|
|
trackRect.y += trackRect.height;
|
|
if (trackRect.y < clip.GetBottom()) {
|
|
AColor::TrackPanelBackground(dc, false);
|
|
dc->DrawRectangle(trackRect.x,
|
|
trackRect.y,
|
|
trackRect.width,
|
|
clip.height - trackRect.y);
|
|
}
|
|
|
|
// Sometimes highlight is not drawn on backing bitmap. I thought
|
|
// it was because FindFocus did not return "this" on Mac, but
|
|
// when I removed that test, yielding this condition:
|
|
// if (GetFocusedTrack() != NULL) {
|
|
// the highlight was reportedly drawn even when something else
|
|
// was the focus and no highlight should be drawn. -RBD
|
|
if (GetFocusedTrack() != NULL && wxWindow::FindFocus() == this) {
|
|
HighlightFocusedTrack(dc, focusRect);
|
|
}
|
|
|
|
// Draw snap guidelines if we have any
|
|
if (mSnapManager && (mSnapLeft >= 0 || mSnapRight >= 0)) {
|
|
AColor::SnapGuidePen(dc);
|
|
if (mSnapLeft >= 0) {
|
|
AColor::Line(*dc, (int)mSnapLeft, 0, mSnapLeft, 30000);
|
|
}
|
|
if (mSnapRight >= 0) {
|
|
AColor::Line(*dc, (int)mSnapRight, 0, mSnapRight, 30000);
|
|
}
|
|
}
|
|
|
|
#ifdef EXPERIMENTAL_SCRUBBING_BASIC
|
|
if (IsScrubbing())
|
|
DrawScrubSpeed(*dc);
|
|
#endif
|
|
}
|
|
|
|
#ifdef EXPERIMENTAL_SCRUBBING_BASIC
|
|
void TrackPanel::DrawScrubSpeed(wxDC &dc)
|
|
{
|
|
// Halt scrubbing and associated display when some other program
|
|
// has focus
|
|
if (!mScrubHasFocus)
|
|
return;
|
|
|
|
const bool seeking = PollIsSeeking();
|
|
if (// Draw for (non-scroll) scrub, sometimes, but never for seek
|
|
(!seeking && mScrubSpeedDisplayCountdown > 0)
|
|
#ifdef EXPERIMENTAL_SCRUBBING_SMOOTH_SCROLL
|
|
// Draw always for scroll-scrub and for scroll-seek
|
|
|| mSmoothScrollingScrub
|
|
#endif
|
|
|
|
) {
|
|
int panelWidth, panelHeight;
|
|
GetSize(&panelWidth, &panelHeight);
|
|
|
|
// Where's the mouse?
|
|
int xx, yy;
|
|
::wxGetMousePosition(&xx, &yy);
|
|
ScreenToClient(&xx, &yy);
|
|
|
|
// Find the text
|
|
const double speed =
|
|
#ifdef EXPERIMENTAL_SCRUBBING_SMOOTH_SCROLL
|
|
mSmoothScrollingScrub
|
|
? seeking
|
|
? FindSeekSpeed(mViewInfo->PositionToTime(xx, GetLeftOffset()))
|
|
: FindScrubSpeed(mViewInfo->PositionToTime(xx, GetLeftOffset()))
|
|
:
|
|
#endif
|
|
mMaxScrubSpeed;
|
|
|
|
const wxChar *format =
|
|
#ifdef EXPERIMENTAL_SCRUBBING_SMOOTH_SCROLL
|
|
mSmoothScrollingScrub
|
|
? seeking
|
|
? wxT("%+.2fX")
|
|
: wxT("%+.2f")
|
|
:
|
|
#endif
|
|
wxT("%.2f");
|
|
|
|
wxString text(wxString::Format(format, speed));
|
|
|
|
static const wxFont labelFont(24, wxSWISS, wxNORMAL, wxNORMAL);
|
|
dc.SetFont(labelFont);
|
|
|
|
// Find the origin for drawing text
|
|
wxCoord width, height;
|
|
dc.GetTextExtent(text, &width, &height);
|
|
xx = std::max(0, std::min(panelWidth - width, xx - width / 2));
|
|
|
|
// Put the text above the cursor, if it fits.
|
|
enum { offset = 20 };
|
|
yy -= height + offset;
|
|
if (yy < 0)
|
|
yy += height + 2 * offset;
|
|
yy = std::max(0, std::min(panelHeight - height, yy));
|
|
|
|
// These two colors were previously saturated red and green. However
|
|
// we have a rule to try to only use red for reserved purposes of
|
|
// (a) Recording
|
|
// (b) Error alerts
|
|
// So they were changed to 'orange' and 'lime'.
|
|
static const wxColour clrNoScroll(215, 162, 0), clrScroll(0, 204, 153);
|
|
#ifdef EXPERIMENTAL_SCRUBBING_SMOOTH_SCROLL
|
|
if (mSmoothScrollingScrub)
|
|
dc.SetTextForeground(clrScroll);
|
|
else
|
|
#endif
|
|
dc.SetTextForeground(clrNoScroll);
|
|
dc.DrawText(text, xx, yy);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
/// Draw zooming indicator that shows the region that will
|
|
/// be zoomed into when the user clicks and drags with a
|
|
/// zoom cursor. Handles both vertical and horizontal
|
|
/// zooming.
|
|
void TrackPanel::DrawZooming(wxDC * dc, const wxRect & clip)
|
|
{
|
|
wxRect r;
|
|
|
|
dc->SetBrush(*wxTRANSPARENT_BRUSH);
|
|
dc->SetPen(*wxBLACK_DASHED_PEN);
|
|
|
|
if (mMouseCapture==IsVZooming) {
|
|
int width, height;
|
|
GetTracksUsableArea(&width, &height);
|
|
|
|
r.y = mZoomStart;
|
|
r.x = GetVRulerOffset();
|
|
r.width = width + GetVRulerWidth() + 1; //+1 extends into border rect
|
|
r.height = mZoomEnd - mZoomStart;
|
|
}
|
|
else {
|
|
r.x = mZoomStart;
|
|
r.y = -1;
|
|
r.width = mZoomEnd - mZoomStart;
|
|
r.height = clip.height + 2;
|
|
}
|
|
|
|
dc->DrawRectangle(r);
|
|
}
|
|
|
|
|
|
void TrackPanel::DrawOutside(Track * t, wxDC * dc, const wxRect & rec,
|
|
const wxRect & trackRect)
|
|
{
|
|
wxRect r = rec;
|
|
int labelw = GetLabelWidth();
|
|
int vrul = GetVRulerOffset();
|
|
|
|
DrawOutsideOfTrack(t, dc, r);
|
|
|
|
r.x += kLeftInset;
|
|
r.y += kTopInset;
|
|
r.width -= kLeftInset * 2;
|
|
r.height -= kTopInset;
|
|
|
|
mTrackInfo.SetTrackInfoFont(dc);
|
|
dc->SetTextForeground(theTheme.Colour(clrTrackPanelText));
|
|
|
|
bool bIsWave = (t->GetKind() == Track::Wave);
|
|
#ifdef USE_MIDI
|
|
bool bIsNote = (t->GetKind() == Track::Note);
|
|
#endif
|
|
// don't enable bHasMuteSolo for Note track because it will draw in the
|
|
// wrong place.
|
|
mTrackInfo.DrawBackground(dc, r, t->GetSelected(), bIsWave, labelw, vrul);
|
|
|
|
// Vaughan, 2010-08-24: No longer doing this.
|
|
// Draw sync-lock tiles in ruler area.
|
|
//if (t->IsSyncLockSelected()) {
|
|
// wxRect tileFill = r;
|
|
// tileFill.x = GetVRulerOffset();
|
|
// tileFill.width = GetVRulerWidth();
|
|
// TrackArtist::DrawSyncLockTiles(dc, tileFill);
|
|
//}
|
|
|
|
DrawBordersAroundTrack(t, dc, r, labelw, vrul);
|
|
DrawShadow(t, dc, r);
|
|
|
|
r.width = mTrackInfo.GetTrackInfoWidth();
|
|
bool captured = (t == mCapturedTrack);
|
|
mTrackInfo.DrawCloseBox(dc, r, (captured && mMouseCapture==IsClosing));
|
|
mTrackInfo.DrawTitleBar(dc, r, t, (captured && mMouseCapture==IsPopping));
|
|
|
|
mTrackInfo.DrawMinimize(dc, r, t, (captured && mMouseCapture==IsMinimizing));
|
|
|
|
// Draw the sync-lock indicator if this track is in a sync-lock selected group.
|
|
if (t->IsSyncLockSelected())
|
|
{
|
|
wxRect syncLockIconRect;
|
|
mTrackInfo.GetSyncLockIconRect(r, syncLockIconRect);
|
|
wxBitmap syncLockBitmap(theTheme.Image(bmpSyncLockIcon));
|
|
// Icon is 12x12 and syncLockIconRect is 16x16.
|
|
dc->DrawBitmap(syncLockBitmap,
|
|
syncLockIconRect.x + 3,
|
|
syncLockIconRect.y + 2,
|
|
true);
|
|
}
|
|
|
|
mTrackInfo.DrawBordersWithin( dc, r, bIsWave );
|
|
|
|
if (bIsWave) {
|
|
mTrackInfo.DrawMuteSolo(dc, r, t, (captured && mMouseCapture == IsMuting), false, HasSoloButton());
|
|
mTrackInfo.DrawMuteSolo(dc, r, t, (captured && mMouseCapture == IsSoloing), true, HasSoloButton());
|
|
|
|
mTrackInfo.DrawSliders(dc, (WaveTrack *)t, r);
|
|
if (!t->GetMinimized()) {
|
|
int offset = 8;
|
|
|
|
if (r.y + 22 + 12 < rec.y + rec.height - 19)
|
|
dc->DrawText(TrackSubText(t),
|
|
trackRect.x + offset,
|
|
trackRect.y + 22);
|
|
|
|
if (r.y + 38 + 12 < rec.y + rec.height - 19)
|
|
dc->DrawText(GetSampleFormatStr(((WaveTrack *) t)->GetSampleFormat()),
|
|
trackRect.x + offset,
|
|
trackRect.y + 38);
|
|
}
|
|
}
|
|
|
|
#ifdef USE_MIDI
|
|
else if (bIsNote) {
|
|
// Note tracks do not have text, e.g. "Mono, 44100Hz, 32-bit float", so
|
|
// Mute & Solo button goes higher. To preserve existing AudioTrack code,
|
|
// we move the buttons up by pretending track is higher (at lower y)
|
|
r.y -= 34;
|
|
r.height += 34;
|
|
wxRect midiRect;
|
|
mTrackInfo.GetTrackControlsRect(trackRect, midiRect);
|
|
// Offset by height of Solo/Mute buttons:
|
|
midiRect.y += 15;
|
|
midiRect.height -= 21; // allow room for minimize button at bottom
|
|
|
|
#ifdef EXPERIMENTAL_MIDI_OUT
|
|
// the offset 2 is just to leave a little space between channel buttons
|
|
// and velocity slider (if any)
|
|
int h = ((NoteTrack *) t)->DrawLabelControls(*dc, midiRect) + 2;
|
|
|
|
// Draw some lines for MuteSolo buttons:
|
|
if (r.height > 84) {
|
|
AColor::Line(*dc, r.x+48 , r.y+50, r.x+48, r.y + 66);
|
|
// bevel below mute/solo
|
|
AColor::Line(*dc, r.x, r.y + 66, mTrackInfo.GetTrackInfoWidth(), r.y + 66);
|
|
}
|
|
mTrackInfo.DrawMuteSolo(dc, r, t,
|
|
(captured && mMouseCapture == IsMuting), false, HasSoloButton());
|
|
mTrackInfo.DrawMuteSolo(dc, r, t,
|
|
(captured && mMouseCapture == IsSoloing), true, HasSoloButton());
|
|
|
|
// place a volume control below channel buttons (this will
|
|
// control an offset to midi velocity).
|
|
// DrawVelocitySlider places slider assuming this is a Wave track
|
|
// and using a large offset to leave room for other things,
|
|
// so here we make a fake rectangle as if it is for a Wave
|
|
// track, but it is offset to place the slider properly in
|
|
// a Note track. This whole placement thing should be redesigned
|
|
// to lay out different types of tracks and controls
|
|
wxRect gr; // gr is gain rectangle where slider is drawn
|
|
mTrackInfo.GetGainRect(r, gr);
|
|
r.y = r.y + h - gr.y; // ultimately want slider at r.y + h
|
|
r.height = r.height - h + gr.y;
|
|
// save for mouse hit detect:
|
|
((NoteTrack *) t)->SetGainPlacementRect(r);
|
|
mTrackInfo.DrawVelocitySlider(dc, (NoteTrack *) t, r);
|
|
#endif
|
|
}
|
|
#endif // USE_MIDI
|
|
}
|
|
|
|
void TrackPanel::DrawOutsideOfTrack(Track * t, wxDC * dc, const wxRect & r)
|
|
{
|
|
// Fill in area outside of the track
|
|
AColor::TrackPanelBackground(dc, false);
|
|
wxRect side;
|
|
|
|
// Area between panel border and left track border
|
|
side = r;
|
|
side.width = kLeftInset;
|
|
dc->DrawRectangle(side);
|
|
|
|
// Area between panel border and top track border
|
|
side = r;
|
|
side.height = kTopInset;
|
|
dc->DrawRectangle(side);
|
|
|
|
// Area between panel border and right track border
|
|
side = r;
|
|
side.x += side.width - kTopInset;
|
|
side.width = kTopInset;
|
|
dc->DrawRectangle(side);
|
|
|
|
// Area between tracks of stereo group
|
|
#ifdef EXPERIMENTAL_OUTPUT_DISPLAY
|
|
if (t->GetLinked() || MONO_WAVE_PAN(t)) {
|
|
side = r;
|
|
side.y += t->GetHeight() - 1;
|
|
side.height = kTopInset + 1;
|
|
dc->DrawRectangle(side);
|
|
}
|
|
#else
|
|
if (t->GetLinked()) {
|
|
side = r;
|
|
side.y += t->GetHeight() - 1;
|
|
side.height = kTopInset + 1;
|
|
dc->DrawRectangle(side);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
/// Draw a three-level highlight gradient around the focused track.
|
|
void TrackPanel::HighlightFocusedTrack(wxDC * dc, const wxRect & r)
|
|
{
|
|
wxRect rect = r;
|
|
rect.x += kLeftInset;
|
|
rect.y += kTopInset;
|
|
rect.width -= kLeftInset * 2;
|
|
rect.height -= kTopInset;
|
|
|
|
dc->SetBrush(*wxTRANSPARENT_BRUSH);
|
|
|
|
AColor::TrackFocusPen(dc, 0);
|
|
dc->DrawRectangle(rect.x - 1, rect.y - 1, rect.width + 2, rect.height + 2);
|
|
|
|
AColor::TrackFocusPen(dc, 1);
|
|
dc->DrawRectangle(rect.x - 2, rect.y - 2, rect.width + 4, rect.height + 4);
|
|
|
|
AColor::TrackFocusPen(dc, 2);
|
|
dc->DrawRectangle(rect.x - 3, rect.y - 3, rect.width + 6, rect.height + 6);
|
|
}
|
|
|
|
void TrackPanel::UpdateVRulers()
|
|
{
|
|
TrackListOfKindIterator iter(Track::Wave, mTracks);
|
|
for (Track *t = iter.First(); t; t = iter.Next()) {
|
|
UpdateTrackVRuler(t);
|
|
}
|
|
|
|
UpdateVRulerSize();
|
|
}
|
|
|
|
void TrackPanel::UpdateVRuler(Track *t)
|
|
{
|
|
UpdateTrackVRuler(t);
|
|
|
|
UpdateVRulerSize();
|
|
}
|
|
|
|
void TrackPanel::UpdateTrackVRuler(Track *t)
|
|
{
|
|
wxASSERT(t);
|
|
if (!t)
|
|
return;
|
|
|
|
wxRect r(GetVRulerOffset(),
|
|
kTopInset,
|
|
GetVRulerWidth(),
|
|
t->GetHeight() - (kTopInset + 2));
|
|
|
|
mTrackArtist->UpdateVRuler(t, r);
|
|
Track *l = t->GetLink();
|
|
if (l)
|
|
{
|
|
r.height = l->GetHeight() - (kTopInset + 2);
|
|
mTrackArtist->UpdateVRuler(l, r);
|
|
}
|
|
#ifdef EXPERIMENTAL_OUTPUT_DISPLAY
|
|
else if(MONO_WAVE_PAN(t)){
|
|
r.height = t->GetHeight(true) - (kTopInset + 2);
|
|
mTrackArtist->UpdateVRuler(t, r);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void TrackPanel::UpdateVRulerSize()
|
|
{
|
|
TrackListIterator iter(mTracks);
|
|
Track *t = iter.First();
|
|
if (t) {
|
|
wxSize s = t->vrulerSize;
|
|
while (t) {
|
|
s.IncTo(t->vrulerSize);
|
|
t = iter.Next();
|
|
}
|
|
if (vrulerSize != s) {
|
|
vrulerSize = s;
|
|
mRuler->SetLeftOffset(GetLeftOffset()); // bevel on AdornedRuler
|
|
mRuler->Refresh();
|
|
}
|
|
}
|
|
Refresh(false);
|
|
}
|
|
|
|
/// The following method moves to the previous track
|
|
/// selecting and unselecting depending if you are on the start of a
|
|
/// block or not.
|
|
|
|
/// \todo Merge related methods, TrackPanel::OnPrevTrack and
|
|
/// TrackPanel::OnNextTrack.
|
|
void TrackPanel::OnPrevTrack( bool shift )
|
|
{
|
|
TrackListIterator iter( mTracks );
|
|
Track* t = GetFocusedTrack();
|
|
if( t == NULL ) // if there isn't one, focus on last
|
|
{
|
|
t = iter.Last();
|
|
SetFocusedTrack( t );
|
|
EnsureVisible( t );
|
|
MakeParentModifyState(false);
|
|
return;
|
|
}
|
|
|
|
Track* p = NULL;
|
|
bool tSelected = false;
|
|
bool pSelected = false;
|
|
if( shift )
|
|
{
|
|
p = mTracks->GetPrev( t, true ); // Get previous track
|
|
if( p == NULL ) // On first track
|
|
{
|
|
// JKC: wxBell() is probably for accessibility, so a blind
|
|
// user knows they were at the top track.
|
|
wxBell();
|
|
if( mCircularTrackNavigation )
|
|
{
|
|
TrackListIterator iter( mTracks );
|
|
p = iter.Last();
|
|
}
|
|
else
|
|
{
|
|
EnsureVisible( t );
|
|
return;
|
|
}
|
|
}
|
|
tSelected = t->GetSelected();
|
|
if (p)
|
|
pSelected = p->GetSelected();
|
|
if( tSelected && pSelected )
|
|
{
|
|
mTracks->Select( t, false );
|
|
SetFocusedTrack( p ); // move focus to next track down
|
|
EnsureVisible( p );
|
|
MakeParentModifyState(false);
|
|
return;
|
|
}
|
|
if( tSelected && !pSelected )
|
|
{
|
|
mTracks->Select( p, true );
|
|
SetFocusedTrack( p ); // move focus to next track down
|
|
EnsureVisible( p );
|
|
MakeParentModifyState(false);
|
|
return;
|
|
}
|
|
if( !tSelected && pSelected )
|
|
{
|
|
mTracks->Select( p, false );
|
|
SetFocusedTrack( p ); // move focus to next track down
|
|
EnsureVisible( p );
|
|
MakeParentModifyState(false);
|
|
return;
|
|
}
|
|
if( !tSelected && !pSelected )
|
|
{
|
|
mTracks->Select( t, true );
|
|
SetFocusedTrack( p ); // move focus to next track down
|
|
EnsureVisible( p );
|
|
MakeParentModifyState(false);
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
p = mTracks->GetPrev( t, true ); // Get next track
|
|
if( p == NULL ) // On last track so stay there?
|
|
{
|
|
wxBell();
|
|
if( mCircularTrackNavigation )
|
|
{
|
|
TrackListIterator iter( mTracks );
|
|
for( Track *d = iter.First(); d; d = iter.Next( true ) )
|
|
{
|
|
p = d;
|
|
}
|
|
SetFocusedTrack( p ); // Wrap to the first track
|
|
EnsureVisible( p );
|
|
MakeParentModifyState(false);
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
EnsureVisible( t );
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
SetFocusedTrack( p ); // move focus to next track down
|
|
EnsureVisible( p );
|
|
MakeParentModifyState(false);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The following method moves to the next track,
|
|
/// selecting and unselecting depending if you are on the start of a
|
|
/// block or not.
|
|
void TrackPanel::OnNextTrack( bool shift )
|
|
{
|
|
Track *t;
|
|
Track *n;
|
|
TrackListIterator iter( mTracks );
|
|
bool tSelected,nSelected;
|
|
|
|
t = GetFocusedTrack(); // Get currently focused track
|
|
if( t == NULL ) // if there isn't one, focus on first
|
|
{
|
|
t = iter.First();
|
|
SetFocusedTrack( t );
|
|
EnsureVisible( t );
|
|
MakeParentModifyState(false);
|
|
return;
|
|
}
|
|
|
|
if( shift )
|
|
{
|
|
n = mTracks->GetNext( t, true ); // Get next track
|
|
if( n == NULL ) // On last track so stay there
|
|
{
|
|
wxBell();
|
|
if( mCircularTrackNavigation )
|
|
{
|
|
TrackListIterator iter( mTracks );
|
|
n = iter.First();
|
|
}
|
|
else
|
|
{
|
|
EnsureVisible( t );
|
|
return;
|
|
}
|
|
}
|
|
tSelected = t->GetSelected();
|
|
nSelected = n->GetSelected();
|
|
if( tSelected && nSelected )
|
|
{
|
|
mTracks->Select( t, false );
|
|
SetFocusedTrack( n ); // move focus to next track down
|
|
EnsureVisible( n );
|
|
MakeParentModifyState(false);
|
|
return;
|
|
}
|
|
if( tSelected && !nSelected )
|
|
{
|
|
mTracks->Select( n, true );
|
|
SetFocusedTrack( n ); // move focus to next track down
|
|
EnsureVisible( n );
|
|
MakeParentModifyState(false);
|
|
return;
|
|
}
|
|
if( !tSelected && nSelected )
|
|
{
|
|
mTracks->Select( n, false );
|
|
SetFocusedTrack( n ); // move focus to next track down
|
|
EnsureVisible( n );
|
|
MakeParentModifyState(false);
|
|
return;
|
|
}
|
|
if( !tSelected && !nSelected )
|
|
{
|
|
mTracks->Select( t, true );
|
|
SetFocusedTrack( n ); // move focus to next track down
|
|
EnsureVisible( n );
|
|
MakeParentModifyState(false);
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
n = mTracks->GetNext( t, true ); // Get next track
|
|
if( n == NULL ) // On last track so stay there
|
|
{
|
|
wxBell();
|
|
if( mCircularTrackNavigation )
|
|
{
|
|
TrackListIterator iter( mTracks );
|
|
n = iter.First();
|
|
SetFocusedTrack( n ); // Wrap to the first track
|
|
EnsureVisible( n );
|
|
MakeParentModifyState(false);
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
EnsureVisible( t );
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
SetFocusedTrack( n ); // move focus to next track down
|
|
EnsureVisible( n );
|
|
MakeParentModifyState(false);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void TrackPanel::OnFirstTrack()
|
|
{
|
|
Track *t = GetFocusedTrack();
|
|
if (!t)
|
|
return;
|
|
|
|
TrackListIterator iter(mTracks);
|
|
Track *f = iter.First();
|
|
if (t != f)
|
|
{
|
|
SetFocusedTrack(f);
|
|
MakeParentModifyState(false);
|
|
}
|
|
EnsureVisible(f);
|
|
}
|
|
|
|
void TrackPanel::OnLastTrack()
|
|
{
|
|
Track *t = GetFocusedTrack();
|
|
if (!t)
|
|
return;
|
|
|
|
TrackListIterator iter(mTracks);
|
|
Track *l = iter.Last();
|
|
if (t != l)
|
|
{
|
|
SetFocusedTrack(l);
|
|
MakeParentModifyState(false);
|
|
}
|
|
EnsureVisible(l);
|
|
}
|
|
|
|
void TrackPanel::OnToggle()
|
|
{
|
|
Track *t;
|
|
|
|
t = GetFocusedTrack(); // Get currently focused track
|
|
if (!t)
|
|
return;
|
|
|
|
mTracks->Select( t, !t->GetSelected() );
|
|
EnsureVisible( t );
|
|
MakeParentModifyState(false);
|
|
|
|
mAx->Updated();
|
|
|
|
return;
|
|
}
|
|
|
|
// Make sure selection edge is in view
|
|
void TrackPanel::ScrollIntoView(double pos)
|
|
{
|
|
const int screenWidth = rint(mViewInfo->GetScreenWidth());
|
|
|
|
int w, h;
|
|
GetTracksUsableArea( &w, &h );
|
|
// Or should we just set w = screenWidth ?
|
|
|
|
int pixel = mViewInfo->TimeToPosition(pos);
|
|
if (pixel < 0 || pixel >= screenWidth)
|
|
{
|
|
mListener->TP_ScrollWindow
|
|
(mViewInfo->OffsetTimeByPixels(pos, -(w / 2)));
|
|
Refresh(false);
|
|
}
|
|
}
|
|
|
|
void TrackPanel::ScrollIntoView(int x)
|
|
{
|
|
ScrollIntoView(mViewInfo->PositionToTime(x, GetLeftOffset()));
|
|
}
|
|
|
|
void TrackPanel::OnCursorLeft( bool shift, bool ctrl, bool keyup )
|
|
{
|
|
// PRL: What I found and preserved, strange though it be:
|
|
// During playback: jump depends on preferences and is independent of the zoom
|
|
// and does not vary if the key is held
|
|
// Else: jump depends on the zoom and gets bigger if the key is held
|
|
int snapToTime = GetActiveProject()->GetSnapTo();
|
|
double quietSeekStepPositive = 1.0; // pixels
|
|
double audioSeekStepPositive = shift ? mSeekLong : mSeekShort;
|
|
SeekLeftOrRight
|
|
(true, shift, ctrl, keyup, snapToTime, true, false,
|
|
quietSeekStepPositive, true,
|
|
audioSeekStepPositive, false);
|
|
}
|
|
|
|
void TrackPanel::OnCursorRight(bool shift, bool ctrl, bool keyup)
|
|
{
|
|
// PRL: What I found and preserved, strange though it be:
|
|
// During playback: jump depends on preferences and is independent of the zoom
|
|
// and does not vary if the key is held
|
|
// Else: jump depends on the zoom and gets bigger if the key is held
|
|
int snapToTime = GetActiveProject()->GetSnapTo();
|
|
double quietSeekStepPositive = 1.0; // pixels
|
|
double audioSeekStepPositive = shift ? mSeekLong : mSeekShort;
|
|
SeekLeftOrRight
|
|
(false, shift, ctrl, keyup, snapToTime, true, false,
|
|
quietSeekStepPositive, true,
|
|
audioSeekStepPositive, false);
|
|
}
|
|
|
|
// Handle small cursor and play head movements
|
|
void TrackPanel::SeekLeftOrRight
|
|
(bool leftward, bool shift, bool ctrl, bool keyup,
|
|
int snapToTime, bool mayAccelerateQuiet, bool mayAccelerateAudio,
|
|
double quietSeekStepPositive, bool quietStepIsPixels,
|
|
double audioSeekStepPositive, bool audioStepIsPixels)
|
|
{
|
|
if (keyup)
|
|
{
|
|
if (IsAudioActive())
|
|
{
|
|
return;
|
|
}
|
|
|
|
MakeParentModifyState(false);
|
|
return;
|
|
}
|
|
|
|
// If the last adjustment was very recent, we are
|
|
// holding the key down and should move faster.
|
|
const wxLongLong curtime = ::wxGetLocalTimeMillis();
|
|
enum { MIN_INTERVAL = 50 };
|
|
const bool fast = (curtime - mLastSelectionAdjustment < MIN_INTERVAL);
|
|
|
|
// How much faster should the cursor move if shift is down?
|
|
enum { LARGER_MULTIPLIER = 4 };
|
|
int multiplier = (fast && mayAccelerateQuiet) ? LARGER_MULTIPLIER : 1;
|
|
if (leftward)
|
|
multiplier = -multiplier;
|
|
|
|
if (shift && ctrl)
|
|
{
|
|
mLastSelectionAdjustment = curtime;
|
|
|
|
// Contract selection
|
|
// Reduce and constrain (counter-intuitive)
|
|
if (leftward) {
|
|
const double t1 = mViewInfo->selectedRegion.t1();
|
|
mViewInfo->selectedRegion.setT1(
|
|
std::max(mViewInfo->selectedRegion.t0(),
|
|
snapToTime
|
|
? GridMove(t1, multiplier)
|
|
: quietStepIsPixels
|
|
? mViewInfo->OffsetTimeByPixels(
|
|
t1, int(multiplier * quietSeekStepPositive))
|
|
: t1 + multiplier * quietSeekStepPositive
|
|
));
|
|
|
|
// Make sure it's visible.
|
|
ScrollIntoView(mViewInfo->selectedRegion.t1());
|
|
}
|
|
else {
|
|
const double t0 = mViewInfo->selectedRegion.t0();
|
|
mViewInfo->selectedRegion.setT0(
|
|
std::min(mViewInfo->selectedRegion.t1(),
|
|
snapToTime
|
|
? GridMove(t0, multiplier)
|
|
: quietStepIsPixels
|
|
? mViewInfo->OffsetTimeByPixels(
|
|
t0, int(multiplier * quietSeekStepPositive))
|
|
: t0 + multiplier * quietSeekStepPositive
|
|
));
|
|
|
|
// Make sure new position is in view.
|
|
ScrollIntoView(mViewInfo->selectedRegion.t0());
|
|
}
|
|
Refresh(false);
|
|
}
|
|
else if (IsAudioActive()) {
|
|
#ifdef EXPERIMENTAL_IMPROVED_SEEKING
|
|
if (gAudioIO->GetLastPlaybackTime() < mLastSelectionAdjustment) {
|
|
// Allow time for the last seek to output a buffer before
|
|
// discarding samples again
|
|
// Do not advance mLastSelectionAdjustment
|
|
return;
|
|
}
|
|
#endif
|
|
mLastSelectionAdjustment = curtime;
|
|
|
|
// Ignore the multiplier for the quiet case
|
|
multiplier = (fast && mayAccelerateAudio) ? LARGER_MULTIPLIER : 1;
|
|
if (leftward)
|
|
multiplier = -multiplier;
|
|
|
|
// If playing, reposition
|
|
double seconds;
|
|
if (audioStepIsPixels) {
|
|
const double streamTime = gAudioIO->GetStreamTime();
|
|
const double newTime =
|
|
mViewInfo->OffsetTimeByPixels(streamTime, int(audioSeekStepPositive));
|
|
seconds = newTime - streamTime;
|
|
}
|
|
else
|
|
seconds = multiplier * audioSeekStepPositive;
|
|
gAudioIO->SeekStream(seconds);
|
|
return;
|
|
}
|
|
else if (shift)
|
|
{
|
|
mLastSelectionAdjustment = curtime;
|
|
|
|
// Extend selection
|
|
// Expand and constrain
|
|
if (leftward) {
|
|
const double t0 = mViewInfo->selectedRegion.t0();
|
|
mViewInfo->selectedRegion.setT0(
|
|
std::max(0.0,
|
|
snapToTime
|
|
? GridMove(t0, multiplier)
|
|
: quietStepIsPixels
|
|
? mViewInfo->OffsetTimeByPixels(
|
|
t0, int(multiplier * quietSeekStepPositive))
|
|
: t0 + multiplier * quietSeekStepPositive
|
|
));
|
|
|
|
// Make sure it's visible.
|
|
ScrollIntoView(mViewInfo->selectedRegion.t0());
|
|
}
|
|
else {
|
|
double end = mTracks->GetEndTime();
|
|
|
|
const double t1 = mViewInfo->selectedRegion.t1();
|
|
mViewInfo->selectedRegion.setT1(
|
|
std::min(end,
|
|
snapToTime
|
|
? GridMove(t1, multiplier)
|
|
: quietStepIsPixels
|
|
? mViewInfo->OffsetTimeByPixels(
|
|
t1, int(multiplier * quietSeekStepPositive))
|
|
: t1 + multiplier * quietSeekStepPositive
|
|
));
|
|
|
|
// Make sure new position is in view.
|
|
ScrollIntoView(mViewInfo->selectedRegion.t1());
|
|
}
|
|
Refresh(false);
|
|
}
|
|
else
|
|
{
|
|
mLastSelectionAdjustment = curtime;
|
|
|
|
// Move the cursor
|
|
// Already in cursor mode?
|
|
if (mViewInfo->selectedRegion.isPoint())
|
|
{
|
|
// Move and constrain
|
|
double end = mTracks->GetEndTime();
|
|
const double t0 = mViewInfo->selectedRegion.t0();
|
|
mViewInfo->selectedRegion.setT0(
|
|
std::max(0.0,
|
|
std::min(end,
|
|
snapToTime
|
|
? GridMove(t0, multiplier)
|
|
: quietStepIsPixels
|
|
? mViewInfo->OffsetTimeByPixels(
|
|
t0, int(multiplier * quietSeekStepPositive))
|
|
: t0 + multiplier * quietSeekStepPositive)),
|
|
false // do not swap selection boundaries
|
|
);
|
|
mViewInfo->selectedRegion.collapseToT0();
|
|
|
|
// Move the visual cursor
|
|
DrawCursor();
|
|
}
|
|
else
|
|
{
|
|
// Transition to cursor mode.
|
|
if (leftward)
|
|
mViewInfo->selectedRegion.collapseToT0();
|
|
else
|
|
mViewInfo->selectedRegion.collapseToT1();
|
|
Refresh(false);
|
|
}
|
|
|
|
// Make sure new position is in view
|
|
ScrollIntoView(mViewInfo->selectedRegion.t1());
|
|
}
|
|
}
|
|
|
|
// Handles moving a selection edge with the keyboard in snap-to-time mode;
|
|
// returns the moved value.
|
|
// Will move at least minPix pixels -- set minPix positive to move forward,
|
|
// negative to move backward.
|
|
double TrackPanel::GridMove(double t, int minPix)
|
|
{
|
|
NumericTextCtrl ttc(NumericConverter::TIME, this, wxID_ANY, wxT(""), 0.0, GetProject()->GetRate());
|
|
ttc.SetFormatName(GetProject()->GetSelectionFormat());
|
|
ttc.SetValue(t);
|
|
|
|
// Try incrementing/decrementing the value; if we've moved far enough we're
|
|
// done
|
|
double result;
|
|
minPix >= 0 ? ttc.Increment() : ttc.Decrement();
|
|
result = ttc.GetValue();
|
|
if (abs(mViewInfo->TimeToPosition(result) - mViewInfo->TimeToPosition(t))
|
|
>= abs(minPix))
|
|
return result;
|
|
|
|
// Otherwise, move minPix pixels, then snap to the time.
|
|
result = mViewInfo->OffsetTimeByPixels(t, minPix);
|
|
ttc.SetValue(result);
|
|
result = ttc.GetValue();
|
|
return result;
|
|
}
|
|
|
|
void TrackPanel::OnBoundaryMove(bool left, bool boundaryContract)
|
|
{
|
|
// Move the left/right selection boundary, to either expand or contract the selection
|
|
// left=true: operate on left boundary; left=false: operate on right boundary
|
|
// boundaryContract=true: contract region; boundaryContract=false: expand region.
|
|
|
|
// If the last adjustment was very recent, we are
|
|
// holding the key down and should move faster.
|
|
wxLongLong curtime = ::wxGetLocalTimeMillis();
|
|
int pixels = 1;
|
|
if( curtime - mLastSelectionAdjustment < 50 )
|
|
{
|
|
pixels = 4;
|
|
}
|
|
mLastSelectionAdjustment = curtime;
|
|
|
|
if (IsAudioActive())
|
|
{
|
|
double indicator = gAudioIO->GetStreamTime();
|
|
if (left) {
|
|
mViewInfo->selectedRegion.setT0(indicator, false);
|
|
}
|
|
else
|
|
{
|
|
mViewInfo->selectedRegion.setT1(indicator);
|
|
}
|
|
|
|
MakeParentModifyState(false);
|
|
Refresh(false);
|
|
}
|
|
else
|
|
{
|
|
// BOUNDARY MOVEMENT
|
|
// Contract selection from the right to the left
|
|
if( boundaryContract )
|
|
{
|
|
if (left) {
|
|
// Reduce and constrain left boundary (counter-intuitive)
|
|
// Move the left boundary by at most the desired number of pixels,
|
|
// but not past the right
|
|
mViewInfo->selectedRegion.setT0(
|
|
std::min(mViewInfo->selectedRegion.t1(),
|
|
mViewInfo->OffsetTimeByPixels(
|
|
mViewInfo->selectedRegion.t0(),
|
|
pixels)));
|
|
|
|
// Make sure it's visible
|
|
ScrollIntoView( mViewInfo->selectedRegion.t0() );
|
|
}
|
|
else
|
|
{
|
|
// Reduce and constrain right boundary (counter-intuitive)
|
|
// Move the left boundary by at most the desired number of pixels,
|
|
// but not past the left
|
|
mViewInfo->selectedRegion.setT1(
|
|
std::max(mViewInfo->selectedRegion.t0(),
|
|
mViewInfo->OffsetTimeByPixels(
|
|
mViewInfo->selectedRegion.t1(),
|
|
-pixels)));
|
|
|
|
// Make sure it's visible
|
|
ScrollIntoView( mViewInfo->selectedRegion.t1() );
|
|
}
|
|
}
|
|
// BOUNDARY MOVEMENT
|
|
// Extend selection toward the left
|
|
else
|
|
{
|
|
if (left) {
|
|
// Expand and constrain left boundary
|
|
mViewInfo->selectedRegion.setT0(
|
|
std::max(0.0,
|
|
mViewInfo->OffsetTimeByPixels(
|
|
mViewInfo->selectedRegion.t0(),
|
|
-pixels)));
|
|
|
|
// Make sure it's visible
|
|
ScrollIntoView( mViewInfo->selectedRegion.t0() );
|
|
}
|
|
else
|
|
{
|
|
// Expand and constrain right boundary
|
|
double end = mTracks->GetEndTime();
|
|
mViewInfo->selectedRegion.setT1(
|
|
std::min(end,
|
|
mViewInfo->OffsetTimeByPixels(
|
|
mViewInfo->selectedRegion.t1(),
|
|
pixels)));
|
|
|
|
// Make sure it's visible
|
|
ScrollIntoView(mViewInfo->selectedRegion.t1());
|
|
}
|
|
}
|
|
Refresh( false );
|
|
MakeParentModifyState(false);
|
|
}
|
|
}
|
|
|
|
// Move the cursor forward or backward, while paused or while playing.
|
|
// forward=true: Move cursor forward; forward=false: Move cursor backwards
|
|
// jump=false: Move cursor determined by zoom; jump=true: Use seek times
|
|
// longjump=false: Use mSeekShort; longjump=true: Use mSeekLong
|
|
void TrackPanel::OnCursorMove(bool forward, bool jump, bool longjump )
|
|
{
|
|
// PRL: nobody calls this yet with !jump
|
|
|
|
double positiveSeekStep;
|
|
bool byPixels;
|
|
if (jump) {
|
|
if (!longjump) {
|
|
positiveSeekStep = mSeekShort;
|
|
} else {
|
|
positiveSeekStep = mSeekLong;
|
|
}
|
|
byPixels = false;
|
|
} else {
|
|
positiveSeekStep = 1.0;
|
|
byPixels = true;
|
|
}
|
|
bool mayAccelerate = !jump;
|
|
SeekLeftOrRight
|
|
(!forward, false, false, false,
|
|
0, mayAccelerate, mayAccelerate,
|
|
positiveSeekStep, byPixels,
|
|
positiveSeekStep, byPixels);
|
|
|
|
MakeParentModifyState(false);
|
|
}
|
|
|
|
//The following methods operate controls on specified tracks,
|
|
//This will pop up the track panning dialog for specified track
|
|
void TrackPanel::OnTrackPan()
|
|
{
|
|
Track *t = GetFocusedTrack();
|
|
if (!t || (t->GetKind() != Track::Wave)) {
|
|
return;
|
|
}
|
|
|
|
LWSlider *slider = mTrackInfo.PanSlider((WaveTrack *) t);
|
|
if (slider->ShowDialog()) {
|
|
SetTrackPan(t, slider);
|
|
}
|
|
}
|
|
|
|
void TrackPanel::OnTrackPanLeft()
|
|
{
|
|
Track *t = GetFocusedTrack();
|
|
if (!t || (t->GetKind() != Track::Wave)) {
|
|
return;
|
|
}
|
|
|
|
LWSlider *slider = mTrackInfo.PanSlider((WaveTrack *) t);
|
|
slider->Decrease(1);
|
|
SetTrackPan(t, slider);
|
|
}
|
|
|
|
void TrackPanel::OnTrackPanRight()
|
|
{
|
|
Track *t = GetFocusedTrack();
|
|
if (!t || (t->GetKind() != Track::Wave)) {
|
|
return;
|
|
}
|
|
|
|
LWSlider *slider = mTrackInfo.PanSlider((WaveTrack *) t);
|
|
slider->Increase(1);
|
|
SetTrackPan(t, slider);
|
|
}
|
|
|
|
void TrackPanel::SetTrackPan(Track * t, LWSlider * s)
|
|
{
|
|
wxASSERT(t);
|
|
if( t->GetKind() != Track::Wave )
|
|
return;
|
|
float newValue = s->Get();
|
|
|
|
WaveTrack *link = (WaveTrack *)mTracks->GetLink(t);
|
|
((WaveTrack*)t)->SetPan(newValue);
|
|
if (link)
|
|
link->SetPan(newValue);
|
|
|
|
MakeParentPushState(_("Adjusted Pan"), _("Pan"), PUSH_CONSOLIDATE );
|
|
|
|
RefreshTrack(t);
|
|
}
|
|
|
|
/// This will pop up the track gain dialog for specified track
|
|
void TrackPanel::OnTrackGain()
|
|
{
|
|
Track *t = GetFocusedTrack();
|
|
if (!t || (t->GetKind() != Track::Wave)) {
|
|
return;
|
|
}
|
|
|
|
LWSlider *slider = mTrackInfo.GainSlider((WaveTrack *) t);
|
|
if (slider->ShowDialog()) {
|
|
SetTrackGain(t, slider);
|
|
}
|
|
}
|
|
|
|
void TrackPanel::OnTrackGainInc()
|
|
{
|
|
Track *t = GetFocusedTrack();
|
|
if (!t || (t->GetKind() != Track::Wave)) {
|
|
return;
|
|
}
|
|
|
|
LWSlider *slider = mTrackInfo.GainSlider((WaveTrack *) t);
|
|
slider->Increase(1);
|
|
SetTrackGain(t, slider);
|
|
}
|
|
|
|
void TrackPanel::OnTrackGainDec()
|
|
{
|
|
Track *t = GetFocusedTrack();
|
|
if (!t || (t->GetKind() != Track::Wave)) {
|
|
return;
|
|
}
|
|
|
|
LWSlider *slider = mTrackInfo.GainSlider((WaveTrack *) t);
|
|
slider->Decrease(1);
|
|
SetTrackGain(t, slider);
|
|
}
|
|
|
|
void TrackPanel::SetTrackGain(Track * t, LWSlider * s)
|
|
{
|
|
wxASSERT(t);
|
|
if( t->GetKind() != Track::Wave )
|
|
return ;
|
|
float newValue = s->Get();
|
|
|
|
WaveTrack *link = (WaveTrack *)mTracks->GetLink(t);
|
|
((WaveTrack*)t)->SetGain(newValue);
|
|
if (link)
|
|
link->SetGain(newValue);
|
|
|
|
MakeParentPushState(_("Adjusted gain"), _("Gain"), PUSH_CONSOLIDATE);
|
|
|
|
RefreshTrack(t);
|
|
}
|
|
|
|
void TrackPanel::OnTrackMenu(Track *t)
|
|
{
|
|
if(!t) {
|
|
t = GetFocusedTrack();
|
|
if(!t) return;
|
|
}
|
|
|
|
mPopupMenuTarget = t;
|
|
|
|
bool canMakeStereo = false;
|
|
Track *next = mTracks->GetNext(t);
|
|
|
|
wxMenu *theMenu = NULL;
|
|
if (t->GetKind() == Track::Time) {
|
|
theMenu = mTimeTrackMenu;
|
|
|
|
TimeTrack *tt = (TimeTrack*) t;
|
|
|
|
theMenu->Enable(OnTimeTrackLinID, tt->GetDisplayLog());
|
|
theMenu->Enable(OnTimeTrackLogID, !tt->GetDisplayLog());
|
|
theMenu->Check(OnTimeTrackLogIntID, tt->GetInterpolateLog());
|
|
}
|
|
|
|
if (t->GetKind() == Track::Wave) {
|
|
theMenu = mWaveTrackMenu;
|
|
if (next && !t->GetLinked() && !next->GetLinked()
|
|
&& t->GetKind() == Track::Wave
|
|
&& next->GetKind() == Track::Wave)
|
|
canMakeStereo = true;
|
|
|
|
theMenu->Enable(OnSwapChannelsID, t->GetLinked());
|
|
theMenu->Enable(OnMergeStereoID, canMakeStereo);
|
|
theMenu->Enable(OnSplitStereoID, t->GetLinked());
|
|
theMenu->Enable(OnSplitStereoMonoID, t->GetLinked());
|
|
|
|
// We only need to set check marks. Clearing checks causes problems on Linux (bug 851)
|
|
switch (t->GetChannel()) {
|
|
case Track::LeftChannel:
|
|
theMenu->Check(OnChannelLeftID, true);
|
|
break;
|
|
case Track::RightChannel:
|
|
theMenu->Check(OnChannelRightID, true);
|
|
break;
|
|
default:
|
|
theMenu->Check(OnChannelMonoID, true);
|
|
}
|
|
|
|
theMenu->Enable(OnChannelMonoID, !t->GetLinked());
|
|
theMenu->Enable(OnChannelLeftID, !t->GetLinked());
|
|
theMenu->Enable(OnChannelRightID, !t->GetLinked());
|
|
|
|
const int display = static_cast<WaveTrack *>(t)->GetDisplay();
|
|
theMenu->Check(
|
|
(display == WaveTrack::Waveform) ? OnWaveformID : OnSpectrumID,
|
|
true
|
|
);
|
|
|
|
WaveTrack * track = (WaveTrack *)t;
|
|
SetMenuCheck(*mRateMenu, IdOfRate((int) track->GetRate()));
|
|
SetMenuCheck(*mFormatMenu, IdOfFormat(track->GetSampleFormat()));
|
|
|
|
bool unsafe = IsUnsafe();
|
|
for (int i = OnRate8ID; i <= OnFloatID; i++) {
|
|
theMenu->Enable(i, !unsafe);
|
|
}
|
|
}
|
|
|
|
#if defined(USE_MIDI)
|
|
if (t->GetKind() == Track::Note)
|
|
theMenu = mNoteTrackMenu;
|
|
#endif
|
|
|
|
if (t->GetKind() == Track::Label){
|
|
theMenu = mLabelTrackMenu;
|
|
}
|
|
|
|
if (theMenu) {
|
|
theMenu->Enable(OnMoveUpID, mTracks->CanMoveUp(t));
|
|
theMenu->Enable(OnMoveDownID, mTracks->CanMoveDown(t));
|
|
theMenu->Enable(OnMoveTopID, mTracks->CanMoveUp(t));
|
|
theMenu->Enable(OnMoveBottomID, mTracks->CanMoveDown(t));
|
|
|
|
//We need to find the location of the menu rectangle.
|
|
wxRect r = FindTrackRect(t,true);
|
|
wxRect titleRect;
|
|
mTrackInfo.GetTitleBarRect(r,titleRect);
|
|
|
|
PopupMenu(theMenu, titleRect.x + 1,
|
|
titleRect.y + titleRect.height + 1);
|
|
}
|
|
|
|
mPopupMenuTarget = NULL;
|
|
|
|
SetCapturedTrack(NULL);
|
|
|
|
Refresh(false);
|
|
}
|
|
|
|
void TrackPanel::OnVRulerMenu(Track *t, wxMouseEvent *pEvent)
|
|
{
|
|
if (!t) {
|
|
t = GetFocusedTrack();
|
|
if (!t)
|
|
return;
|
|
}
|
|
|
|
if (t->GetKind() != Track::Wave)
|
|
return;
|
|
|
|
WaveTrack *const wt = static_cast<WaveTrack*>(t);
|
|
|
|
const int display = wt->GetDisplay();
|
|
wxMenu *theMenu;
|
|
if (display == WaveTrack::Waveform) {
|
|
theMenu = mRulerWaveformMenu;
|
|
const int id =
|
|
OnFirstWaveformScaleID + int(wt->GetWaveformSettings().scaleType);
|
|
theMenu->Check(id, true);
|
|
}
|
|
else {
|
|
theMenu = mRulerSpectrumMenu;
|
|
const int id =
|
|
OnFirstSpectrumScaleID + int(wt->GetSpectrogramSettings().scaleType);
|
|
theMenu->Check(id, true);
|
|
}
|
|
|
|
int x, y;
|
|
if (pEvent)
|
|
x = pEvent->m_x, y = pEvent->m_y;
|
|
else {
|
|
// If no event given, pop up the menu at the same height
|
|
// as for the track control menu
|
|
const wxRect r = FindTrackRect(wt, true);
|
|
wxRect titleRect;
|
|
mTrackInfo.GetTitleBarRect(r, titleRect);
|
|
x = GetVRulerOffset(), y = titleRect.y + titleRect.height + 1;
|
|
}
|
|
|
|
// So that IsDragZooming() returns false, and if we zoom in, we do so
|
|
// centered where the mouse is now:
|
|
mZoomStart = mZoomEnd = pEvent->m_y;
|
|
|
|
mPopupMenuTarget = wt;
|
|
PopupMenu(theMenu, x, y);
|
|
mPopupMenuTarget = NULL;
|
|
}
|
|
|
|
void TrackPanel::OnTrackMute(bool shiftDown, Track *t)
|
|
{
|
|
if (!t) {
|
|
t = GetFocusedTrack();
|
|
if (!t || (t->GetKind() != Track::Wave))
|
|
return;
|
|
}
|
|
GetProject()->HandleTrackMute(t, shiftDown);
|
|
|
|
// Update mixer board, too.
|
|
MixerBoard* pMixerBoard = this->GetMixerBoard();
|
|
if (pMixerBoard)
|
|
{
|
|
pMixerBoard->UpdateMute(); // Update for all tracks.
|
|
pMixerBoard->UpdateSolo(); // Update for all tracks.
|
|
}
|
|
|
|
mAx->Updated();
|
|
Refresh(false);
|
|
}
|
|
|
|
|
|
void TrackPanel::OnTrackSolo(bool shiftDown, Track *t)
|
|
{
|
|
if (!t)
|
|
{
|
|
t = GetFocusedTrack();
|
|
if (!t || (t->GetKind() != Track::Wave))
|
|
return;
|
|
}
|
|
GetProject()->HandleTrackSolo(t, shiftDown);
|
|
|
|
// Update mixer board, too.
|
|
MixerBoard* pMixerBoard = this->GetMixerBoard();
|
|
if (pMixerBoard)
|
|
{
|
|
pMixerBoard->UpdateMute(); // Update for all tracks.
|
|
pMixerBoard->UpdateSolo(); // Update for all tracks.
|
|
}
|
|
|
|
mAx->Updated();
|
|
Refresh(false);
|
|
}
|
|
|
|
void TrackPanel::OnTrackClose()
|
|
{
|
|
Track *t = GetFocusedTrack();
|
|
if(!t) return;
|
|
|
|
if (IsUnsafe())
|
|
{
|
|
mListener->TP_DisplayStatusMessage( _( "Can't delete track with active audio" ) );
|
|
wxBell();
|
|
return;
|
|
}
|
|
|
|
RemoveTrack( t );
|
|
|
|
SetCapturedTrack( NULL );
|
|
|
|
// BG: There are no more tracks on screen
|
|
if( mTracks->IsEmpty() )
|
|
{
|
|
//BG: Set zoom to normal
|
|
mViewInfo->SetZoom(ZoomInfo::GetDefaultZoom());
|
|
|
|
//STM: Set selection to 0,0
|
|
//PRL: and default the rest of the selection information
|
|
mViewInfo->selectedRegion = SelectedRegion();
|
|
|
|
mListener->TP_RedrawScrollbars();
|
|
mListener->TP_DisplayStatusMessage( wxT( "" ) ); //STM: Clear message if all tracks are removed
|
|
}
|
|
|
|
Refresh( false );
|
|
}
|
|
|
|
void TrackPanel::OnTrackMoveUp()
|
|
{
|
|
if (mTracks->CanMoveUp(GetFocusedTrack()))
|
|
MoveTrack(GetFocusedTrack(), OnMoveUpID);
|
|
}
|
|
|
|
void TrackPanel::OnTrackMoveDown()
|
|
{
|
|
if (mTracks->CanMoveDown(GetFocusedTrack()))
|
|
MoveTrack(GetFocusedTrack(), OnMoveDownID);
|
|
}
|
|
|
|
void TrackPanel::OnTrackMoveTop()
|
|
{
|
|
if (mTracks->CanMoveUp(GetFocusedTrack()))
|
|
MoveTrack(GetFocusedTrack(), OnMoveTopID);
|
|
}
|
|
|
|
void TrackPanel::OnTrackMoveBottom()
|
|
{
|
|
if (mTracks->CanMoveDown(GetFocusedTrack()))
|
|
MoveTrack(GetFocusedTrack(), OnMoveBottomID);
|
|
}
|
|
|
|
|
|
Track * TrackPanel::GetFirstSelectedTrack()
|
|
{
|
|
|
|
TrackListIterator iter(mTracks);
|
|
|
|
Track * t;
|
|
for ( t = iter.First();t!=NULL;t=iter.Next())
|
|
{
|
|
//Find the first selected track
|
|
if(t->GetSelected())
|
|
{
|
|
return t;
|
|
}
|
|
|
|
}
|
|
//if nothing is selected, return the first track
|
|
t = iter.First();
|
|
|
|
if(t)
|
|
return t;
|
|
else
|
|
return NULL;
|
|
}
|
|
|
|
void TrackPanel::EnsureVisible(Track * t)
|
|
{
|
|
TrackListIterator iter(mTracks);
|
|
Track *it = NULL;
|
|
Track *nt = NULL;
|
|
|
|
SetFocusedTrack(t);
|
|
|
|
int trackTop = 0;
|
|
int trackHeight =0;
|
|
|
|
for (it = iter.First(); it; it = iter.Next()) {
|
|
trackTop += trackHeight;
|
|
trackHeight = it->GetHeight();
|
|
|
|
//find the second track if this is stereo
|
|
if (it->GetLinked()) {
|
|
nt = iter.Next();
|
|
trackHeight += nt->GetHeight();
|
|
}
|
|
#ifdef EXPERIMENTAL_OUTPUT_DISPLAY
|
|
else if(MONO_WAVE_PAN(it)){
|
|
trackHeight += it->GetHeight(true);
|
|
}
|
|
#endif
|
|
else {
|
|
nt = it;
|
|
}
|
|
|
|
//We have found the track we want to ensure is visible.
|
|
if ((it == t) || (nt == t)) {
|
|
|
|
//Get the size of the trackpanel.
|
|
int width, height;
|
|
GetSize(&width, &height);
|
|
|
|
if (trackTop < mViewInfo->vpos) {
|
|
height = mViewInfo->vpos - trackTop + mViewInfo->scrollStep;
|
|
height /= mViewInfo->scrollStep;
|
|
mListener->TP_ScrollUpDown(-height);
|
|
}
|
|
else if (trackTop + trackHeight > mViewInfo->vpos + height) {
|
|
height = (trackTop + trackHeight) - (mViewInfo->vpos + height);
|
|
height = (height + mViewInfo->scrollStep + 1) / mViewInfo->scrollStep;
|
|
mListener->TP_ScrollUpDown(height);
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
Refresh(false);
|
|
}
|
|
|
|
void TrackPanel::DrawBordersAroundTrack(Track * t, wxDC * dc,
|
|
const wxRect & r, const int vrul,
|
|
const int labelw)
|
|
{
|
|
// Border around track and label area
|
|
dc->SetBrush(*wxTRANSPARENT_BRUSH);
|
|
dc->SetPen(*wxBLACK_PEN);
|
|
dc->DrawRectangle(r.x, r.y, r.width - 1, r.height - 1);
|
|
|
|
AColor::Line(*dc, labelw, r.y, labelw, r.y + r.height - 1); // between vruler and TrackInfo
|
|
|
|
// The lines at bottom of 1st track and top of second track of stereo group
|
|
// Possibly replace with DrawRectangle to add left border.
|
|
#ifdef EXPERIMENTAL_OUTPUT_DISPLAY
|
|
if (t->GetLinked() || MONO_WAVE_PAN(t)) {
|
|
int h1 = r.y + t->GetHeight() - kTopInset;
|
|
AColor::Line(*dc, vrul, h1 - 2, r.x + r.width - 1, h1 - 2);
|
|
AColor::Line(*dc, vrul, h1 + kTopInset, r.x + r.width - 1, h1 + kTopInset);
|
|
}
|
|
#else
|
|
if (t->GetLinked()) {
|
|
int h1 = r.y + t->GetHeight() - kTopInset;
|
|
AColor::Line(*dc, vrul, h1 - 2, r.x + r.width - 1, h1 - 2);
|
|
AColor::Line(*dc, vrul, h1 + kTopInset, r.x + r.width - 1, h1 + kTopInset);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void TrackPanel::DrawShadow(Track * /* t */ , wxDC * dc, const wxRect & r)
|
|
{
|
|
int right = r.x + r.width - 1;
|
|
int bottom = r.y + r.height - 1;
|
|
|
|
// shadow
|
|
dc->SetPen(*wxBLACK_PEN);
|
|
|
|
// bottom
|
|
AColor::Line(*dc, r.x, bottom, right, bottom);
|
|
// right
|
|
AColor::Line(*dc, right, r.y, right, bottom);
|
|
|
|
// background
|
|
AColor::Dark(dc, false);
|
|
|
|
// bottom
|
|
AColor::Line(*dc, r.x, bottom, r.x + 1, bottom);
|
|
// right
|
|
AColor::Line(*dc, right, r.y, right, r.y + 1);
|
|
}
|
|
|
|
/// Returns the string to be displayed in the track label
|
|
/// indicating whether the track is mono, left, right, or
|
|
/// stereo and what sample rate it's using.
|
|
wxString TrackPanel::TrackSubText(Track * t)
|
|
{
|
|
wxString s = wxString::Format(wxT("%dHz"),
|
|
(int) (((WaveTrack *) t)->GetRate() +
|
|
0.5));
|
|
#ifdef EXPERIMENTAL_OUTPUT_DISPLAY
|
|
if (t->GetLinked() && t->GetChannel() != Track::MonoChannel)
|
|
s = _("Stereo, ") + s;
|
|
#else
|
|
if (t->GetLinked())
|
|
s = _("Stereo, ") + s;
|
|
#endif
|
|
else {
|
|
if (t->GetChannel() == Track::MonoChannel)
|
|
s = _("Mono, ") + s;
|
|
else if (t->GetChannel() == Track::LeftChannel)
|
|
s = _("Left, ") + s;
|
|
else if (t->GetChannel() == Track::RightChannel)
|
|
s = _("Right, ") + s;
|
|
}
|
|
|
|
return s;
|
|
}
|
|
|
|
/// Handle the menu options that change a track between
|
|
/// left channel, right channel, and mono.
|
|
static int channels[] = { Track::LeftChannel, Track::RightChannel,
|
|
Track::MonoChannel
|
|
};
|
|
|
|
static const wxChar *channelmsgs[] = { _("Left Channel"), _("Right Channel"),
|
|
_("Mono")
|
|
};
|
|
|
|
void TrackPanel::OnChannelChange(wxCommandEvent & event)
|
|
{
|
|
int id = event.GetId();
|
|
wxASSERT(id >= OnChannelLeftID && id <= OnChannelMonoID);
|
|
wxASSERT(mPopupMenuTarget);
|
|
mPopupMenuTarget->SetChannel(channels[id - OnChannelLeftID]);
|
|
MakeParentPushState(wxString::Format(_("Changed '%s' to %s"),
|
|
mPopupMenuTarget->GetName().c_str(),
|
|
channelmsgs[id - OnChannelLeftID]),
|
|
_("Channel"));
|
|
Refresh(false);
|
|
}
|
|
|
|
/// Swap the left and right channels of a stero track...
|
|
void TrackPanel::OnSwapChannels(wxCommandEvent & WXUNUSED(event))
|
|
{
|
|
Track *partner = mPopupMenuTarget->GetLink();
|
|
SplitStereo(true);
|
|
mPopupMenuTarget->SetChannel(Track::RightChannel);
|
|
partner->SetChannel(Track::LeftChannel);
|
|
|
|
(mTracks->MoveUp(partner));
|
|
partner->SetLinked(true);
|
|
|
|
MixerBoard* pMixerBoard = this->GetMixerBoard();
|
|
if (pMixerBoard) {
|
|
pMixerBoard->UpdateTrackClusters();
|
|
}
|
|
|
|
MakeParentPushState(wxString::Format(_("Swapped Channels in '%s'"),
|
|
mPopupMenuTarget->GetName().c_str()),
|
|
_("Swap Channels"));
|
|
|
|
}
|
|
|
|
/// Split a stereo track into two tracks...
|
|
void TrackPanel::OnSplitStereo(wxCommandEvent & WXUNUSED(event))
|
|
{
|
|
SplitStereo(true);
|
|
MakeParentPushState(wxString::Format(_("Split stereo track '%s'"),
|
|
mPopupMenuTarget->GetName().c_str()),
|
|
_("Split"));
|
|
}
|
|
|
|
/// Split a stereo track into two mono tracks...
|
|
void TrackPanel::OnSplitStereoMono(wxCommandEvent & WXUNUSED(event))
|
|
{
|
|
SplitStereo(false);
|
|
MakeParentPushState(wxString::Format(_("Split Stereo to Mono '%s'"),
|
|
mPopupMenuTarget->GetName().c_str()),
|
|
_("Split to Mono"));
|
|
}
|
|
|
|
/// Split a stereo track into two tracks...
|
|
void TrackPanel::SplitStereo(bool stereo)
|
|
{
|
|
wxASSERT(mPopupMenuTarget);
|
|
|
|
if (!stereo)
|
|
mPopupMenuTarget->SetChannel(Track::MonoChannel);
|
|
|
|
Track *partner = mPopupMenuTarget->GetLink();
|
|
|
|
#ifdef EXPERIMENTAL_OUTPUT_DISPLAY
|
|
if(!stereo && MONO_WAVE_PAN(mPopupMenuTarget))
|
|
((WaveTrack*)mPopupMenuTarget)->SetVirtualState(true,true);
|
|
if(!stereo && MONO_WAVE_PAN(partner))
|
|
((WaveTrack*)partner)->SetVirtualState(true,true);
|
|
#endif
|
|
|
|
if (partner)
|
|
{
|
|
partner->SetName(mPopupMenuTarget->GetName());
|
|
if (!stereo)
|
|
partner->SetChannel(Track::MonoChannel); // Keep original stereo track name.
|
|
|
|
//On Demand - have each channel add it's own.
|
|
if (ODManager::IsInstanceCreated() && partner->GetKind() == Track::Wave)
|
|
ODManager::Instance()->MakeWaveTrackIndependent((WaveTrack*)partner);
|
|
}
|
|
|
|
mPopupMenuTarget->SetLinked(false);
|
|
//make sure neither track is smaller than its minimum height
|
|
if (mPopupMenuTarget->GetHeight() < mPopupMenuTarget->GetMinimizedHeight())
|
|
mPopupMenuTarget->SetHeight(mPopupMenuTarget->GetMinimizedHeight());
|
|
if (partner)
|
|
{
|
|
if (partner->GetHeight() < partner->GetMinimizedHeight())
|
|
partner->SetHeight(partner->GetMinimizedHeight());
|
|
|
|
// Make tracks the same height
|
|
if (mPopupMenuTarget->GetHeight() != partner->GetHeight())
|
|
{
|
|
mPopupMenuTarget->SetHeight(((mPopupMenuTarget->GetHeight())+(partner->GetHeight())) / 2.0);
|
|
partner->SetHeight(mPopupMenuTarget->GetHeight());
|
|
}
|
|
}
|
|
|
|
Refresh(false);
|
|
}
|
|
|
|
/// Merge two tracks into one stereo track ??
|
|
void TrackPanel::OnMergeStereo(wxCommandEvent & WXUNUSED(event))
|
|
{
|
|
wxASSERT(mPopupMenuTarget);
|
|
mPopupMenuTarget->SetLinked(true);
|
|
Track *partner = mPopupMenuTarget->GetLink();
|
|
|
|
#ifdef EXPERIMENTAL_OUTPUT_DISPLAY
|
|
if(MONO_WAVE_PAN(mPopupMenuTarget))
|
|
((WaveTrack*)mPopupMenuTarget)->SetVirtualState(false);
|
|
if(MONO_WAVE_PAN(partner))
|
|
((WaveTrack*)partner)->SetVirtualState(false);
|
|
#endif
|
|
|
|
if (partner) {
|
|
// Set partner's parameters to match target.
|
|
partner->Merge(*mPopupMenuTarget);
|
|
|
|
mPopupMenuTarget->SetChannel(Track::LeftChannel);
|
|
partner->SetChannel(Track::RightChannel);
|
|
|
|
// Set new track heights and minimized state
|
|
bool bBothMinimizedp=((mPopupMenuTarget->GetMinimized())&&(partner->GetMinimized()));
|
|
mPopupMenuTarget->SetMinimized(false);
|
|
partner->SetMinimized(false);
|
|
int AverageHeight=(mPopupMenuTarget->GetHeight() + partner->GetHeight())/ 2;
|
|
mPopupMenuTarget->SetHeight(AverageHeight);
|
|
partner->SetHeight(AverageHeight);
|
|
mPopupMenuTarget->SetMinimized(bBothMinimizedp);
|
|
partner->SetMinimized(bBothMinimizedp);
|
|
|
|
//On Demand - join the queues together.
|
|
if(ODManager::IsInstanceCreated() && partner->GetKind() == Track::Wave && mPopupMenuTarget->GetKind() == Track::Wave )
|
|
if(!ODManager::Instance()->MakeWaveTrackDependent((WaveTrack*)partner,(WaveTrack*)mPopupMenuTarget))
|
|
{
|
|
;
|
|
//TODO: in the future, we will have to check the return value of MakeWaveTrackDependent -
|
|
//if the tracks cannot merge, it returns false, and in that case we should not allow a merging.
|
|
//for example it returns false when there are two different types of ODTasks on each track's queue.
|
|
//we will need to display this to the user.
|
|
}
|
|
|
|
MakeParentPushState(wxString::Format(_("Made '%s' a stereo track"),
|
|
mPopupMenuTarget->GetName().
|
|
c_str()),
|
|
_("Make Stereo"));
|
|
} else
|
|
mPopupMenuTarget->SetLinked(false);
|
|
|
|
Refresh(false);
|
|
}
|
|
|
|
class ViewSettingsDialog : public PrefsDialog
|
|
{
|
|
public:
|
|
ViewSettingsDialog
|
|
(wxWindow *parent, const wxString &title, PrefsDialog::Factories &factories,
|
|
int page)
|
|
: PrefsDialog(parent, title, factories)
|
|
, mPage(page)
|
|
{
|
|
}
|
|
|
|
virtual long GetPreferredPage()
|
|
{
|
|
return mPage;
|
|
}
|
|
|
|
virtual void SavePreferredPage()
|
|
{
|
|
}
|
|
|
|
private:
|
|
const int mPage;
|
|
};
|
|
|
|
void TrackPanel::OnViewSettings(wxCommandEvent &)
|
|
{
|
|
WaveTrack *const wt = static_cast<WaveTrack*>(mPopupMenuTarget);
|
|
WaveformPrefsFactory waveformFactory(wt);
|
|
SpectrumPrefsFactory spectrumFactory(wt);
|
|
|
|
// Put Waveform page first
|
|
PrefsDialog::Factories factories;
|
|
factories.push_back(&waveformFactory);
|
|
factories.push_back(&spectrumFactory);
|
|
const int page = (wt->GetDisplay() == WaveTrack::Spectrum)
|
|
? 1 : 0;
|
|
|
|
wxString title(wt->GetName() + wxT(": "));
|
|
ViewSettingsDialog dialog(this, title, factories, page);
|
|
|
|
if (0 != dialog.ShowModal())
|
|
// Redraw
|
|
Refresh(false);
|
|
}
|
|
|
|
/// Set the Display mode based on the menu choice in the Track Menu.
|
|
/// Note that gModes MUST BE IN THE SAME ORDER AS THE MENU CHOICES!!
|
|
/// const wxChar *gModes[] = { wxT("waveform"), wxT("waveformDB"),
|
|
/// wxT("spectrum"), wxT("pitch") };
|
|
void TrackPanel::OnSetDisplay(wxCommandEvent & event)
|
|
{
|
|
int idInt = event.GetId();
|
|
wxASSERT(idInt >= OnWaveformID && idInt <= OnSpectrumID);
|
|
wxASSERT(mPopupMenuTarget
|
|
&& mPopupMenuTarget->GetKind() == Track::Wave);
|
|
|
|
WaveTrack::WaveTrackDisplay id;
|
|
switch (idInt) {
|
|
default:
|
|
case OnWaveformID:
|
|
id = WaveTrack::Waveform; break;
|
|
case OnSpectrumID:
|
|
id = WaveTrack::Spectrum; break;
|
|
}
|
|
WaveTrack *wt = (WaveTrack *) mPopupMenuTarget;
|
|
if (wt->GetDisplay() != id) {
|
|
wt->SetDisplay(WaveTrack::WaveTrackDisplay(id));
|
|
|
|
WaveTrack *l = static_cast<WaveTrack *>(wt->GetLink());
|
|
if (l)
|
|
l->SetDisplay(WaveTrack::WaveTrackDisplay(id));
|
|
#ifdef EXPERIMENTAL_OUTPUT_DISPLAY
|
|
if (wt->GetDisplay() == WaveTrack::WaveformDisplay) {
|
|
wt->SetVirtualState(false);
|
|
}else if (id == WaveTrack::WaveformDisplay) {
|
|
wt->SetVirtualState(true);
|
|
}
|
|
#endif
|
|
UpdateVRuler(wt);
|
|
|
|
MakeParentModifyState(true);
|
|
Refresh(false);
|
|
}
|
|
}
|
|
|
|
/// Sets the sample rate for a track, and if it is linked to
|
|
/// another track, that one as well.
|
|
void TrackPanel::SetRate(Track * pTrack, double rate)
|
|
{
|
|
((WaveTrack *) pTrack)->SetRate(rate);
|
|
Track *partner = mTracks->GetLink(pTrack);
|
|
if (partner)
|
|
((WaveTrack *) partner)->SetRate(rate);
|
|
// Separate conversion of "rate" enables changing the decimals without affecting i18n
|
|
wxString rateString = wxString::Format(wxT("%.3f"), rate);
|
|
MakeParentPushState(wxString::Format(_("Changed '%s' to %s Hz"),
|
|
pTrack->GetName().c_str(), rateString.c_str()),
|
|
_("Rate Change"));
|
|
}
|
|
|
|
/// Handles the selection from the Format submenu of the
|
|
/// track menu.
|
|
void TrackPanel::OnFormatChange(wxCommandEvent & event)
|
|
{
|
|
int id = event.GetId();
|
|
wxASSERT(id >= On16BitID && id <= OnFloatID);
|
|
wxASSERT(mPopupMenuTarget
|
|
&& mPopupMenuTarget->GetKind() == Track::Wave);
|
|
|
|
sampleFormat newFormat = int16Sample;
|
|
|
|
switch (id) {
|
|
case On16BitID:
|
|
newFormat = int16Sample;
|
|
break;
|
|
case On24BitID:
|
|
newFormat = int24Sample;
|
|
break;
|
|
case OnFloatID:
|
|
newFormat = floatSample;
|
|
break;
|
|
default:
|
|
// ERROR -- should not happen
|
|
wxASSERT(false);
|
|
break;
|
|
}
|
|
if (newFormat == ((WaveTrack*)mPopupMenuTarget)->GetSampleFormat())
|
|
return; // Nothing to do.
|
|
|
|
bool bResult = ((WaveTrack*)mPopupMenuTarget)->ConvertToSampleFormat(newFormat);
|
|
wxASSERT(bResult); // TO DO: Actually handle this.
|
|
Track *partner = mTracks->GetLink(mPopupMenuTarget);
|
|
if (partner)
|
|
{
|
|
bResult = ((WaveTrack*)partner)->ConvertToSampleFormat(newFormat);
|
|
wxASSERT(bResult); // TO DO: Actually handle this.
|
|
}
|
|
|
|
MakeParentPushState(wxString::Format(_("Changed '%s' to %s"),
|
|
mPopupMenuTarget->GetName().
|
|
c_str(),
|
|
GetSampleFormatStr(newFormat)),
|
|
_("Format Change"));
|
|
|
|
SetMenuCheck( *mFormatMenu, id );
|
|
MakeParentRedrawScrollbars();
|
|
Refresh(false);
|
|
}
|
|
|
|
/// Converts a format enumeration to a wxWidgets menu item Id.
|
|
int TrackPanel::IdOfFormat( int format )
|
|
{
|
|
switch (format) {
|
|
case int16Sample:
|
|
return On16BitID;
|
|
case int24Sample:
|
|
return On24BitID;
|
|
case floatSample:
|
|
return OnFloatID;
|
|
default:
|
|
// ERROR -- should not happen
|
|
wxASSERT( false );
|
|
break;
|
|
}
|
|
return OnFloatID;// Compiler food.
|
|
}
|
|
|
|
/// Puts a check mark at a given position in a menu.
|
|
void TrackPanel::SetMenuCheck( wxMenu & menu, int newId )
|
|
{
|
|
wxMenuItemList & list = menu.GetMenuItems();
|
|
wxMenuItem * item;
|
|
int id;
|
|
|
|
for ( wxMenuItemList::compatibility_iterator node = list.GetFirst(); node; node = node->GetNext() )
|
|
{
|
|
item = node->GetData();
|
|
id = item->GetId();
|
|
// We only need to set check marks. Clearing checks causes problems on Linux (bug 851)
|
|
if (id==newId)
|
|
menu.Check( id, true );
|
|
}
|
|
}
|
|
|
|
const int nRates=12;
|
|
|
|
/// gRates MUST CORRESPOND DIRECTLY TO THE RATES AS LISTED IN THE MENU!!
|
|
/// IN THE SAME ORDER!!
|
|
static int gRates[nRates] = { 8000, 11025, 16000, 22050, 44100, 48000, 88200, 96000,
|
|
176400, 192000, 352800, 384000 };
|
|
|
|
/// This method handles the selection from the Rate
|
|
/// submenu of the track menu, except for "Other" (/see OnRateOther).
|
|
void TrackPanel::OnRateChange(wxCommandEvent & event)
|
|
{
|
|
int id = event.GetId();
|
|
wxASSERT(id >= OnRate8ID && id <= OnRate384ID);
|
|
wxASSERT(mPopupMenuTarget
|
|
&& mPopupMenuTarget->GetKind() == Track::Wave);
|
|
|
|
SetMenuCheck( *mRateMenu, id );
|
|
SetRate(mPopupMenuTarget, gRates[id - OnRate8ID]);
|
|
|
|
MakeParentRedrawScrollbars();
|
|
|
|
Refresh(false);
|
|
}
|
|
|
|
/// Converts a sampling rate to a wxWidgets menu item id
|
|
int TrackPanel::IdOfRate( int rate )
|
|
{
|
|
for(int i=0;i<nRates;i++) {
|
|
if( gRates[i] == rate )
|
|
return i+OnRate8ID;
|
|
}
|
|
return OnRateOtherID;
|
|
}
|
|
|
|
void TrackPanel::OnRateOther(wxCommandEvent &event)
|
|
{
|
|
wxASSERT(mPopupMenuTarget
|
|
&& mPopupMenuTarget->GetKind() == Track::Wave);
|
|
|
|
int newRate;
|
|
|
|
/// \todo Remove artificial constants!!
|
|
/// \todo Make a real dialog box out of this!!
|
|
while (true)
|
|
{
|
|
wxDialog dlg(this, wxID_ANY, wxString(_("Set Rate")));
|
|
dlg.SetName(dlg.GetTitle());
|
|
ShuttleGui S(&dlg, eIsCreating);
|
|
wxString rate;
|
|
wxArrayString rates;
|
|
wxComboBox *cb;
|
|
|
|
rate.Printf(wxT("%ld"), lrint(((WaveTrack *) mPopupMenuTarget)->GetRate()));
|
|
|
|
rates.Add(wxT("8000"));
|
|
rates.Add(wxT("11025"));
|
|
rates.Add(wxT("16000"));
|
|
rates.Add(wxT("22050"));
|
|
rates.Add(wxT("44100"));
|
|
rates.Add(wxT("48000"));
|
|
rates.Add(wxT("88200"));
|
|
rates.Add(wxT("96000"));
|
|
rates.Add(wxT("176400"));
|
|
rates.Add(wxT("192000"));
|
|
rates.Add(wxT("352800"));
|
|
rates.Add(wxT("384000"));
|
|
|
|
S.StartVerticalLay(true);
|
|
{
|
|
S.SetBorder(10);
|
|
S.StartHorizontalLay(wxEXPAND, false);
|
|
{
|
|
cb = S.AddCombo(_("New sample rate (Hz):"),
|
|
rate,
|
|
&rates);
|
|
#if defined(__WXMAC__)
|
|
// As of wxMac-2.8.12, setting manually is required
|
|
// to handle rates not in the list. See: Bug #427
|
|
cb->SetValue(rate);
|
|
#endif
|
|
}
|
|
S.EndHorizontalLay();
|
|
S.AddStandardButtons();
|
|
}
|
|
S.EndVerticalLay();
|
|
|
|
dlg.SetClientSize(dlg.GetSizer()->CalcMin());
|
|
dlg.Center();
|
|
|
|
if (dlg.ShowModal() != wxID_OK)
|
|
{
|
|
return; // user cancelled dialog
|
|
}
|
|
|
|
long lrate;
|
|
if (cb->GetValue().ToLong(&lrate) && lrate >= 1 && lrate <= 1000000)
|
|
{
|
|
newRate = (int)lrate;
|
|
break;
|
|
}
|
|
|
|
wxMessageBox(_("The entered value is invalid"), _("Error"),
|
|
wxICON_ERROR, this);
|
|
}
|
|
|
|
SetMenuCheck( *mRateMenu, event.GetId() );
|
|
SetRate(mPopupMenuTarget, newRate);
|
|
|
|
MakeParentRedrawScrollbars();
|
|
Refresh(false);
|
|
}
|
|
|
|
void TrackPanel::OnSetTimeTrackRange(wxCommandEvent & /*event*/)
|
|
{
|
|
TimeTrack *t = (TimeTrack*)mPopupMenuTarget;
|
|
|
|
if (t) {
|
|
long lower = (long) (t->GetRangeLower() * 100.0 + 0.5);
|
|
long upper = (long) (t->GetRangeUpper() * 100.0 + 0.5);
|
|
|
|
// MB: these lower/upper limits match the maximum allowed range of the time track
|
|
// envelope, but this is not strictly required
|
|
lower = wxGetNumberFromUser(_("Change lower speed limit (%) to:"),
|
|
_("Lower speed limit"),
|
|
_("Lower speed limit"),
|
|
lower,
|
|
10,
|
|
1000);
|
|
|
|
upper = wxGetNumberFromUser(_("Change upper speed limit (%) to:"),
|
|
_("Upper speed limit"),
|
|
_("Upper speed limit"),
|
|
upper,
|
|
lower+1,
|
|
1000);
|
|
|
|
if( lower >= 10 && upper <= 1000 && lower < upper ) {
|
|
t->SetRangeLower((double)lower / 100.0);
|
|
t->SetRangeUpper((double)upper / 100.0);
|
|
MakeParentPushState(wxString::Format(_("Set range to '%ld' - '%ld'"),
|
|
lower,
|
|
upper),
|
|
/* i18n-hint: (verb)*/
|
|
|
|
_("Set Range"));
|
|
Refresh(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
void TrackPanel::OnTimeTrackLin(wxCommandEvent & /*event*/)
|
|
{
|
|
TimeTrack *t = (TimeTrack*)mPopupMenuTarget;
|
|
t->SetDisplayLog(false);
|
|
UpdateVRuler(t);
|
|
MakeParentPushState(_("Set time track display to linear"), _("Set Display"));
|
|
Refresh(false);
|
|
}
|
|
|
|
void TrackPanel::OnTimeTrackLog(wxCommandEvent & /*event*/)
|
|
{
|
|
TimeTrack *t = (TimeTrack*)mPopupMenuTarget;
|
|
t->SetDisplayLog(true);
|
|
UpdateVRuler(t);
|
|
MakeParentPushState(_("Set time track display to logarithmic"), _("Set Display"));
|
|
Refresh(false);
|
|
}
|
|
|
|
void TrackPanel::OnTimeTrackLogInt(wxCommandEvent & /*event*/)
|
|
{
|
|
TimeTrack *t = (TimeTrack*)mPopupMenuTarget;
|
|
if(t->GetInterpolateLog()) {
|
|
t->SetInterpolateLog(false);
|
|
MakeParentPushState(_("Set time track interpolation to linear"), _("Set Interpolation"));
|
|
} else {
|
|
t->SetInterpolateLog(true);
|
|
MakeParentPushState(_("Set time track interpolation to logarithmic"), _("Set Interpolation"));
|
|
}
|
|
Refresh(false);
|
|
}
|
|
|
|
void TrackPanel::OnWaveformScaleType(wxCommandEvent &evt)
|
|
{
|
|
WaveTrack *const wt = static_cast<WaveTrack *>(mPopupMenuTarget);
|
|
const WaveformSettings::ScaleType newScaleType =
|
|
WaveformSettings::ScaleType(
|
|
std::max(0,
|
|
std::min(int(WaveformSettings::stNumScaleTypes) - 1,
|
|
evt.GetId() - OnFirstWaveformScaleID
|
|
)));
|
|
if (wt->GetWaveformSettings().scaleType != newScaleType) {
|
|
wt->GetIndependentWaveformSettings().scaleType = newScaleType;
|
|
UpdateVRuler(wt); // Is this really needed?
|
|
MakeParentModifyState(true);
|
|
Refresh(false);
|
|
}
|
|
}
|
|
|
|
void TrackPanel::OnSpectrumScaleType(wxCommandEvent &evt)
|
|
{
|
|
WaveTrack *const wt = static_cast<WaveTrack *>(mPopupMenuTarget);
|
|
const SpectrogramSettings::ScaleType newScaleType =
|
|
SpectrogramSettings::ScaleType(
|
|
std::max(0,
|
|
std::min(int(SpectrogramSettings::stNumScaleTypes) - 1,
|
|
evt.GetId() - OnFirstSpectrumScaleID
|
|
)));
|
|
if (wt->GetSpectrogramSettings().scaleType != newScaleType) {
|
|
wt->GetIndependentSpectrogramSettings().scaleType = newScaleType;
|
|
UpdateVRuler(wt); // Is this really needed?
|
|
MakeParentModifyState(true);
|
|
Refresh(false);
|
|
}
|
|
}
|
|
|
|
void TrackPanel::OnZoomInVertical(wxCommandEvent &)
|
|
{
|
|
HandleWaveTrackVZoom(static_cast<WaveTrack*>(mPopupMenuTarget), false, false);
|
|
}
|
|
|
|
void TrackPanel::OnZoomOutVertical(wxCommandEvent &)
|
|
{
|
|
HandleWaveTrackVZoom(static_cast<WaveTrack*>(mPopupMenuTarget), true, false);
|
|
}
|
|
|
|
void TrackPanel::OnZoomFitVertical(wxCommandEvent &)
|
|
{
|
|
HandleWaveTrackVZoom(static_cast<WaveTrack*>(mPopupMenuTarget), true, true);
|
|
}
|
|
|
|
/// Move a track up, down, to top or to bottom.
|
|
|
|
void TrackPanel::OnMoveTrack(wxCommandEvent &event)
|
|
{
|
|
wxASSERT(event.GetId() == OnMoveUpID || event.GetId() == OnMoveDownID ||
|
|
event.GetId() == OnMoveTopID || event.GetId() == OnMoveBottomID);
|
|
|
|
MoveTrack( mPopupMenuTarget, event.GetId() );
|
|
}
|
|
|
|
void TrackPanel::MoveTrack( Track* target, int eventId )
|
|
{
|
|
wxString direction;
|
|
|
|
switch (eventId)
|
|
{
|
|
case OnMoveTopID :
|
|
/* i18n-hint: where the track is moving to.*/
|
|
direction = _("to Top");
|
|
|
|
while (mTracks->CanMoveUp(target)) {
|
|
if (mTracks->Move(target, true)) {
|
|
MixerBoard* pMixerBoard = this->GetMixerBoard(); // Update mixer board.
|
|
if (pMixerBoard && (target->GetKind() == Track::Wave))
|
|
pMixerBoard->MoveTrackCluster((WaveTrack*)target, true);
|
|
}
|
|
}
|
|
break;
|
|
case OnMoveBottomID :
|
|
/* i18n-hint: where the track is moving to.*/
|
|
direction = _("to Bottom");
|
|
|
|
while (mTracks->CanMoveDown(target)) {
|
|
if (mTracks->Move(target, false)) {
|
|
MixerBoard* pMixerBoard = this->GetMixerBoard(); // Update mixer board.
|
|
if (pMixerBoard && (target->GetKind() == Track::Wave))
|
|
pMixerBoard->MoveTrackCluster((WaveTrack*)target, false);
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
bool bUp = (OnMoveUpID == eventId);
|
|
/* i18n-hint: a direction.*/
|
|
direction = bUp ? _("Up") : _("Down");
|
|
|
|
if (mTracks->Move(target, bUp)) {
|
|
MixerBoard* pMixerBoard = this->GetMixerBoard();
|
|
if (pMixerBoard && (target->GetKind() == Track::Wave)) {
|
|
pMixerBoard->MoveTrackCluster((WaveTrack*)target, bUp);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* i18n-hint: Past tense of 'to move', as in 'moved audio track up'.*/
|
|
wxString longDesc = (_("Moved"));
|
|
/* i18n-hint: The direction of movement will be up, down, to top or to bottom.. */
|
|
wxString shortDesc = (_("Move Track"));
|
|
|
|
longDesc = (wxString::Format(wxT("%s '%s' %s"), longDesc.c_str(),
|
|
target->GetName().c_str(), direction.c_str()));
|
|
shortDesc = (wxString::Format(wxT("%s %s"), shortDesc.c_str(), direction.c_str()));
|
|
|
|
MakeParentPushState(longDesc, shortDesc);
|
|
|
|
Refresh(false);
|
|
}
|
|
|
|
/// This only applies to MIDI tracks. Presumably, it shifts the
|
|
/// whole sequence by an octave.
|
|
void TrackPanel::OnChangeOctave(wxCommandEvent & event)
|
|
{
|
|
#if defined(USE_MIDI)
|
|
wxASSERT(event.GetId() == OnUpOctaveID
|
|
|| event.GetId() == OnDownOctaveID);
|
|
wxASSERT(mPopupMenuTarget->GetKind() == Track::Note);
|
|
NoteTrack *t = (NoteTrack *) mPopupMenuTarget;
|
|
|
|
bool bDown = (OnDownOctaveID == event.GetId());
|
|
t->SetBottomNote(t->GetBottomNote() + ((bDown) ? -12 : 12));
|
|
|
|
MakeParentModifyState(true);
|
|
Refresh(false);
|
|
#endif
|
|
}
|
|
|
|
void TrackPanel::OnSetName(wxCommandEvent & WXUNUSED(event))
|
|
{
|
|
Track *t = mPopupMenuTarget;
|
|
if (t)
|
|
{
|
|
wxString oldName = t->GetName();
|
|
wxString newName =
|
|
wxGetTextFromUser(_("Change track name to:"),
|
|
_("Track Name"), oldName);
|
|
if (newName != wxT("")) // wxGetTextFromUser returns empty string on Cancel.
|
|
{
|
|
t->SetName(newName);
|
|
// if we have a linked channel this name should change as well
|
|
// (otherwise sort by name and time will crash).
|
|
if (t->GetLinked())
|
|
t->GetLink()->SetName(newName);
|
|
|
|
MixerBoard* pMixerBoard = this->GetMixerBoard();
|
|
if (pMixerBoard && (t->GetKind() == Track::Wave))
|
|
pMixerBoard->UpdateName((WaveTrack*)t);
|
|
|
|
MakeParentPushState(wxString::Format(_("Renamed '%s' to '%s'"),
|
|
oldName.c_str(),
|
|
newName.c_str()),
|
|
_("Name Change"));
|
|
|
|
Refresh(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Cut selected text if cut menu item is selected
|
|
void TrackPanel::OnCutSelectedText(wxCommandEvent & WXUNUSED(event))
|
|
{
|
|
LabelTrack *lt = (LabelTrack *)mPopupMenuTarget;
|
|
if (lt->CutSelectedText()) {
|
|
MakeParentPushState(_("Modified Label"),
|
|
_("Label Edit"),
|
|
PUSH_CONSOLIDATE);
|
|
}
|
|
RefreshTrack(lt);
|
|
}
|
|
|
|
/// Copy selected text if copy menu item is selected
|
|
void TrackPanel::OnCopySelectedText(wxCommandEvent & WXUNUSED(event))
|
|
{
|
|
LabelTrack *lt = (LabelTrack *)mPopupMenuTarget;
|
|
lt->CopySelectedText();
|
|
RefreshTrack(lt);
|
|
}
|
|
|
|
/// paste selected text if paste menu item is selected
|
|
void TrackPanel::OnPasteSelectedText(wxCommandEvent & WXUNUSED(event))
|
|
{
|
|
LabelTrack *lt = (LabelTrack *)mPopupMenuTarget;
|
|
if (lt->PasteSelectedText(mViewInfo->selectedRegion.t0(),
|
|
mViewInfo->selectedRegion.t1())) {
|
|
MakeParentPushState(_("Modified Label"),
|
|
_("Label Edit"),
|
|
true /* consolidate */);
|
|
}
|
|
RefreshTrack(lt);
|
|
}
|
|
|
|
/// delete selected label
|
|
void TrackPanel::OnDeleteSelectedLabel(wxCommandEvent & WXUNUSED(event))
|
|
{
|
|
LabelTrack *lt = (LabelTrack *)mPopupMenuTarget;
|
|
int ndx = lt->GetLabelIndex(mViewInfo->selectedRegion.t0(),
|
|
mViewInfo->selectedRegion.t1());
|
|
if (ndx != -1) {
|
|
lt->DeleteLabel(ndx);
|
|
MakeParentPushState(_("Deleted Label"),
|
|
_("Label Edit"),
|
|
true /* consolidate */);
|
|
}
|
|
RefreshTrack(lt);
|
|
}
|
|
|
|
// Small helper class to enumerate all fonts in the system
|
|
// We use this because the default implementation of
|
|
// wxFontEnumerator::GetFacenames() has changed between wx2.6 and 2.8
|
|
class TrackPanelFontEnumerator : public wxFontEnumerator
|
|
{
|
|
public:
|
|
TrackPanelFontEnumerator(wxArrayString* fontNames) :
|
|
mFontNames(fontNames) {}
|
|
|
|
virtual bool OnFacename(const wxString& font)
|
|
{
|
|
mFontNames->Add(font);
|
|
return true;
|
|
}
|
|
|
|
private:
|
|
wxArrayString* mFontNames;
|
|
};
|
|
|
|
void TrackPanel::OnSetFont(wxCommandEvent & WXUNUSED(event))
|
|
{
|
|
wxArrayString facenames;
|
|
TrackPanelFontEnumerator fontEnumerator(&facenames);
|
|
fontEnumerator.EnumerateFacenames(wxFONTENCODING_SYSTEM, false);
|
|
|
|
wxString facename = gPrefs->Read(wxT("/GUI/LabelFontFacename"), wxT(""));
|
|
long fontsize = gPrefs->Read(wxT("/GUI/LabelFontSize"), 12);
|
|
|
|
/* i18n-hint: (noun) This is the font for the label track.*/
|
|
wxDialog dlg(this, wxID_ANY, wxString(_("Label Track Font")));
|
|
dlg.SetName(dlg.GetTitle());
|
|
ShuttleGui S(&dlg, eIsCreating);
|
|
wxListBox *lb;
|
|
wxSpinCtrl *sc;
|
|
|
|
S.StartVerticalLay(true);
|
|
{
|
|
S.StartMultiColumn(2, wxEXPAND);
|
|
{
|
|
S.SetStretchyRow(0);
|
|
S.SetStretchyCol(1);
|
|
|
|
/* i18n-hint: (noun) The name of the typeface*/
|
|
S.AddPrompt(_("Face name"));
|
|
lb = new wxListBox(&dlg, wxID_ANY,
|
|
wxDefaultPosition,
|
|
wxDefaultSize,
|
|
facenames,
|
|
wxLB_SINGLE);
|
|
|
|
lb->SetName(_("Face name"));
|
|
lb->SetSelection(facenames.Index(facename));
|
|
S.AddWindow(lb, wxALIGN_LEFT | wxEXPAND | wxALL);
|
|
|
|
/* i18n-hint: (noun) The size of the typeface*/
|
|
S.AddPrompt(_("Face size"));
|
|
sc = new wxSpinCtrl(&dlg, wxID_ANY,
|
|
wxString::Format(wxT("%ld"), fontsize),
|
|
wxDefaultPosition,
|
|
wxDefaultSize,
|
|
wxSP_ARROW_KEYS,
|
|
8, 48, fontsize);
|
|
sc->SetName(_("Face size"));
|
|
S.AddWindow(sc, wxALIGN_LEFT | wxALL);
|
|
}
|
|
S.EndMultiColumn();
|
|
S.AddStandardButtons();
|
|
}
|
|
S.EndVerticalLay();
|
|
|
|
dlg.Layout();
|
|
dlg.Fit();
|
|
dlg.CenterOnParent();
|
|
if (dlg.ShowModal() == wxID_CANCEL) {
|
|
return;
|
|
}
|
|
|
|
gPrefs->Write(wxT("/GUI/LabelFontFacename"), lb->GetStringSelection());
|
|
gPrefs->Write(wxT("/GUI/LabelFontSize"), sc->GetValue());
|
|
gPrefs->Flush();
|
|
|
|
LabelTrack::ResetFont();
|
|
|
|
Refresh(false);
|
|
}
|
|
|
|
/// Determines which track is under the mouse
|
|
/// @param mouseX - mouse X position.
|
|
/// @param mouseY - mouse Y position.
|
|
/// @param label - true iff the X Y position is relative to side-panel with the labels in it.
|
|
/// @param link - true iff we should consider a hit in any linked track as a hit.
|
|
/// @param *trackRect - returns track rectangle.
|
|
Track *TrackPanel::FindTrack(int mouseX, int mouseY, bool label, bool link,
|
|
wxRect * trackRect)
|
|
{
|
|
wxRect r;
|
|
r.x = 0;
|
|
r.y = -mViewInfo->vpos;
|
|
r.y += kTopInset;
|
|
GetSize(&r.width, &r.height);
|
|
|
|
if (label) {
|
|
r.width = GetLeftOffset();
|
|
} else {
|
|
r.x = GetLeftOffset();
|
|
r.width -= GetLeftOffset();
|
|
}
|
|
|
|
VisibleTrackIterator iter(GetProject());
|
|
for (Track * t = iter.First(); t; t = iter.Next()) {
|
|
r.y = t->GetY() - mViewInfo->vpos + kTopInset;
|
|
r.height = t->GetHeight();
|
|
|
|
if (link && t->GetLink()) {
|
|
Track *l = t->GetLink();
|
|
int h = l->GetHeight();
|
|
if (!t->GetLinked()) {
|
|
t = l;
|
|
r.y = t->GetY() - mViewInfo->vpos + kTopInset;
|
|
}
|
|
r.height += h;
|
|
}
|
|
#ifdef EXPERIMENTAL_OUTPUT_DISPLAY
|
|
else if(link && MONO_WAVE_PAN(t))
|
|
{
|
|
r.height += t->GetHeight(true);
|
|
}
|
|
#endif
|
|
//Determine whether the mouse is inside
|
|
//the current rectangle. If so, recalculate
|
|
//the proper dimensions and return.
|
|
if (r.Contains(mouseX, mouseY)) {
|
|
#ifdef EXPERIMENTAL_OUTPUT_DISPLAY
|
|
t->SetVirtualStereo(false);
|
|
#endif
|
|
if (trackRect) {
|
|
r.y -= kTopInset;
|
|
if (label) {
|
|
r.x += kLeftInset;
|
|
r.width -= kLeftInset;
|
|
r.y += kTopInset;
|
|
r.height -= kTopInset;
|
|
}
|
|
*trackRect = r;
|
|
}
|
|
|
|
return t;
|
|
}
|
|
#ifdef EXPERIMENTAL_OUTPUT_DISPLAY
|
|
if(!link && MONO_WAVE_PAN(t)){
|
|
r.y = t->GetY(true) - mViewInfo->vpos + kTopInset;
|
|
r.height = t->GetHeight(true);
|
|
if (r.Contains(mouseX, mouseY)) {
|
|
t->SetVirtualStereo(true);
|
|
if (trackRect) {
|
|
r.y -= kTopInset;
|
|
if (label) {
|
|
r.x += kLeftInset;
|
|
r.width -= kLeftInset;
|
|
r.y += kTopInset;
|
|
r.height -= kTopInset;
|
|
}
|
|
*trackRect = r;
|
|
}
|
|
return t;
|
|
}
|
|
}
|
|
#endif // EXPERIMENTAL_OUTPUT_DISPLAY
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/// This finds the rectangle of a given track, either the
|
|
/// of the label 'adornment' or the track itself
|
|
wxRect TrackPanel::FindTrackRect(Track * target, bool label)
|
|
{
|
|
if (!target) {
|
|
return wxRect(0,0,0,0);
|
|
}
|
|
|
|
wxRect r(0,
|
|
target->GetY() - mViewInfo->vpos,
|
|
GetSize().GetWidth(),
|
|
target->GetHeight());
|
|
|
|
// The check for a null linked track is necessary because there's
|
|
// a possible race condition between the time the 2 linked tracks
|
|
// are added and when wxAccessible methods are called. This is
|
|
// most evident when using Jaws.
|
|
if (target->GetLinked() && target->GetLink()) {
|
|
r.height += target->GetLink()->GetHeight();
|
|
}
|
|
|
|
if (label) {
|
|
r.x += kLeftInset;
|
|
r.width -= kLeftInset;
|
|
r.y += kTopInset;
|
|
r.height -= kTopInset;
|
|
}
|
|
|
|
return r;
|
|
}
|
|
|
|
int TrackPanel::GetVRulerWidth() const
|
|
{
|
|
return vrulerSize.x;
|
|
}
|
|
|
|
/// Displays the bounds of the selection in the status bar.
|
|
void TrackPanel::DisplaySelection()
|
|
{
|
|
if (!mListener)
|
|
return;
|
|
|
|
// DM: Note that the Selection Bar can actually MODIFY the selection
|
|
// if snap-to mode is on!!!
|
|
mListener->TP_DisplaySelection();
|
|
}
|
|
|
|
bool TrackPanel::MoveClipToTrack(WaveClip *clip, WaveTrack* dst)
|
|
{
|
|
WaveTrack *src = NULL;
|
|
WaveClip *clip2 = NULL;
|
|
WaveTrack *src2 = NULL;
|
|
WaveTrack *dst2 = NULL;
|
|
size_t i;
|
|
|
|
#ifdef USE_MIDI
|
|
// dst could be a note track. Can't move clip to a note track.
|
|
// EXPLAIN: How could dst be a note track (pointer)? It's declared to be a WaveTrack*. I think this test is pointless.
|
|
if (dst->GetKind() != Track::Wave) return false;
|
|
#endif
|
|
|
|
for (i = 0; i < mCapturedClipArray.size(); i++) {
|
|
if (clip == mCapturedClipArray[i].clip) {
|
|
src = (WaveTrack*)mCapturedClipArray[i].track;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!src)
|
|
return false;
|
|
|
|
// Make sure we have the first track of two stereo tracks
|
|
// with both source and destination
|
|
if (!src->GetLinked() && mTracks->GetLink(src)) {
|
|
// set it to NULL in case there is no L channel clip
|
|
clip = NULL;
|
|
|
|
// find the first track by getting the linked track from src
|
|
// assumes that mCapturedArray[i].clip and .track is not NULL.
|
|
for (i = 0; i < mCapturedClipArray.size(); i++) {
|
|
if (mTracks->GetLink(src) == mCapturedClipArray[i].track) {
|
|
clip = mCapturedClipArray[i].clip;
|
|
break;
|
|
}
|
|
}
|
|
|
|
src = (WaveTrack*)mTracks->GetLink(src);
|
|
}
|
|
if (!dst->GetLinked() && mTracks->GetLink(dst))
|
|
dst = (WaveTrack*)mTracks->GetLink(dst);
|
|
|
|
// Get the second track of two stereo tracks
|
|
src2 = (WaveTrack*)mTracks->GetLink(src);
|
|
dst2 = (WaveTrack*)mTracks->GetLink(dst);
|
|
|
|
for (i = 0; i < mCapturedClipArray.size(); i++) {
|
|
if (mCapturedClipArray[i].track == src2) {
|
|
clip2 = mCapturedClipArray[i].clip;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ((src2 && !dst2) || (dst2 && !src2))
|
|
return false; // cannot move stereo- to mono track or other way around
|
|
|
|
// if only the right clip of a stereo pair is being dragged, use clip instead of clip2 to get mono behavior.
|
|
if (!clip && clip2) {
|
|
clip = clip2;
|
|
src = src2;
|
|
dst = dst2;
|
|
clip2 = NULL;
|
|
src2 = dst2 = NULL;
|
|
}
|
|
|
|
if (!dst->CanInsertClip(clip))
|
|
return false;
|
|
|
|
if (clip2) {
|
|
// we should have a source and dest track
|
|
if (!dst2 || !src2)
|
|
return false;
|
|
|
|
if (!dst2->CanInsertClip(clip2))
|
|
return false;
|
|
}
|
|
|
|
src->MoveClipToTrack(clip, dst);
|
|
if (src2)
|
|
src2->MoveClipToTrack(clip2, dst2);
|
|
|
|
// update the captured clip array.
|
|
for (i = 0; i < mCapturedClipArray.size(); i++) {
|
|
if (clip && mCapturedClipArray[i].clip == clip) {
|
|
mCapturedClipArray[i].track = dst;
|
|
} else if (clip2 && mCapturedClipArray[i].clip == clip2) {
|
|
mCapturedClipArray[i].track = dst2;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
Track *TrackPanel::GetFocusedTrack()
|
|
{
|
|
return mAx->GetFocus();
|
|
}
|
|
|
|
void TrackPanel::SetFocusedTrack( Track *t )
|
|
{
|
|
// Make sure we always have the first linked track of a stereo track
|
|
if (t && !t->GetLinked() && t->GetLink())
|
|
t = (WaveTrack*)t->GetLink();
|
|
|
|
if (AudacityProject::GetKeyboardCaptureHandler()) {
|
|
AudacityProject::ReleaseKeyboard(this);
|
|
}
|
|
|
|
if (t && t->GetKind() == Track::Label) {
|
|
AudacityProject::CaptureKeyboard(this);
|
|
}
|
|
|
|
mAx->SetFocus( t );
|
|
Refresh( false );
|
|
}
|
|
|
|
void TrackPanel::OnSetFocus(wxFocusEvent & WXUNUSED(event))
|
|
{
|
|
#ifdef EXPERIMENTAL_SCRUBBING_BASIC
|
|
mScrubHasFocus = IsScrubbing();
|
|
#endif
|
|
SetFocusedTrack( GetFocusedTrack() );
|
|
Refresh( false );
|
|
}
|
|
|
|
void TrackPanel::OnKillFocus(wxFocusEvent & WXUNUSED(event))
|
|
{
|
|
#ifdef EXPERIMENTAL_SCRUBBING_BASIC
|
|
mScrubHasFocus = false;
|
|
#endif
|
|
Refresh( false);
|
|
}
|
|
|
|
/**********************************************************************
|
|
|
|
TrackInfo code is destined to move out of this file.
|
|
Code should become a lot cleaner when we have sizers.
|
|
|
|
**********************************************************************/
|
|
|
|
TrackInfo::TrackInfo(TrackPanel * pParentIn)
|
|
{
|
|
pParent = pParentIn;
|
|
|
|
wxRect r(0, 0, 1000, 1000);
|
|
wxRect sliderRect;
|
|
|
|
GetGainRect(r, sliderRect);
|
|
|
|
/* i18n-hint: Title of the Gain slider, used to adjust the volume */
|
|
mGain = new LWSlider(pParent, _("Gain"),
|
|
wxPoint(sliderRect.x, sliderRect.y),
|
|
wxSize(sliderRect.width, sliderRect.height),
|
|
DB_SLIDER);
|
|
mGain->SetDefaultValue(1.0);
|
|
|
|
GetPanRect(r, sliderRect);
|
|
|
|
/* i18n-hint: Title of the Pan slider, used to move the sound left or right */
|
|
mPan = new LWSlider(pParent, _("Pan"),
|
|
wxPoint(sliderRect.x, sliderRect.y),
|
|
wxSize(sliderRect.width, sliderRect.height),
|
|
PAN_SLIDER);
|
|
mPan->SetDefaultValue(0.0);
|
|
|
|
int fontSize = 10;
|
|
mFont.Create(fontSize, wxFONTFAMILY_SWISS, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL);
|
|
|
|
int allowableWidth = GetTrackInfoWidth() - 2; // 2 to allow for left/right borders
|
|
int textWidth, textHeight;
|
|
do {
|
|
mFont.SetPointSize(fontSize);
|
|
pParent->GetTextExtent(_("Stereo, 999999Hz"),
|
|
&textWidth,
|
|
&textHeight,
|
|
NULL,
|
|
NULL,
|
|
&mFont);
|
|
fontSize--;
|
|
} while (textWidth >= allowableWidth);
|
|
}
|
|
|
|
TrackInfo::~TrackInfo()
|
|
{
|
|
delete mGain;
|
|
delete mPan;
|
|
}
|
|
|
|
static const int kTrackInfoWidth = 100;
|
|
static const int kTrackInfoBtnSize = 16; // widely used dimension, usually height
|
|
|
|
int TrackInfo::GetTrackInfoWidth() const
|
|
{
|
|
return kTrackInfoWidth;
|
|
}
|
|
|
|
void TrackInfo::GetCloseBoxRect(const wxRect & r, wxRect & dest) const
|
|
{
|
|
dest.x = r.x;
|
|
dest.y = r.y;
|
|
dest.width = kTrackInfoBtnSize;
|
|
dest.height = kTrackInfoBtnSize;
|
|
}
|
|
|
|
void TrackInfo::GetTitleBarRect(const wxRect & r, wxRect & dest) const
|
|
{
|
|
dest.x = r.x + kTrackInfoBtnSize; // to right of CloseBoxRect
|
|
dest.y = r.y;
|
|
dest.width = kTrackInfoWidth - r.x - kTrackInfoBtnSize; // to right of CloseBoxRect
|
|
dest.height = kTrackInfoBtnSize;
|
|
}
|
|
|
|
void TrackInfo::GetMuteSoloRect(const wxRect & r, wxRect & dest, bool solo, bool bHasSoloButton) const
|
|
{
|
|
dest.x = r.x ;
|
|
dest.y = r.y + 50;
|
|
dest.width = 48;
|
|
dest.height = kTrackInfoBtnSize;
|
|
|
|
if( !bHasSoloButton )
|
|
{
|
|
dest.width +=48;
|
|
}
|
|
else if (solo)
|
|
{
|
|
dest.x += 48;
|
|
}
|
|
}
|
|
|
|
void TrackInfo::GetGainRect(const wxRect & r, wxRect & dest) const
|
|
{
|
|
dest.x = r.x + 7;
|
|
dest.y = r.y + 70;
|
|
dest.width = 84;
|
|
dest.height = 25;
|
|
}
|
|
|
|
void TrackInfo::GetPanRect(const wxRect & r, wxRect & dest) const
|
|
{
|
|
dest.x = r.x + 7;
|
|
dest.y = r.y + 100;
|
|
dest.width = 84;
|
|
dest.height = 25;
|
|
}
|
|
|
|
void TrackInfo::GetMinimizeRect(const wxRect & r, wxRect &dest) const
|
|
{
|
|
const int kBlankWidth = kTrackInfoBtnSize + 4;
|
|
dest.x = r.x + kBlankWidth;
|
|
dest.y = r.y + r.height - 19;
|
|
// Width is kTrackInfoWidth less space on left for track select and on right for sync-lock icon.
|
|
dest.width = kTrackInfoWidth - (2 * kBlankWidth);
|
|
dest.height = kTrackInfoBtnSize;
|
|
}
|
|
|
|
void TrackInfo::GetSyncLockIconRect(const wxRect & r, wxRect &dest) const
|
|
{
|
|
dest.x = r.x + kTrackInfoWidth - kTrackInfoBtnSize - 4; // to right of minimize button
|
|
dest.y = r.y + r.height - 19;
|
|
dest.width = kTrackInfoBtnSize;
|
|
dest.height = kTrackInfoBtnSize;
|
|
}
|
|
|
|
|
|
/// \todo Probably should move to 'Utils.cpp'.
|
|
void TrackInfo::SetTrackInfoFont(wxDC * dc) const
|
|
{
|
|
dc->SetFont(mFont);
|
|
}
|
|
|
|
void TrackInfo::DrawBordersWithin(wxDC* dc, const wxRect & r, bool bHasMuteSolo) const
|
|
{
|
|
AColor::Dark(dc, false); // same color as border of toolbars (ToolBar::OnPaint())
|
|
|
|
// below close box and title bar
|
|
AColor::Line(*dc, r.x, r.y + kTrackInfoBtnSize, kTrackInfoWidth, r.y + kTrackInfoBtnSize);
|
|
|
|
// between close box and title bar
|
|
AColor::Line(*dc, r.x + kTrackInfoBtnSize, r.y, r.x + kTrackInfoBtnSize, r.y + kTrackInfoBtnSize);
|
|
|
|
if( bHasMuteSolo && (r.height > (66+18) ))
|
|
{
|
|
AColor::Line(*dc, r.x, r.y + 50, kTrackInfoWidth, r.y + 50); // above mute/solo
|
|
AColor::Line(*dc, r.x + 48 , r.y + 50, r.x + 48, r.y + 66); // between mute/solo
|
|
AColor::Line(*dc, r.x, r.y + 66, kTrackInfoWidth, r.y + 66); // below mute/solo
|
|
}
|
|
|
|
// left of and above minimize button
|
|
wxRect minimizeRect;
|
|
this->GetMinimizeRect(r, minimizeRect);
|
|
AColor::Line(*dc, minimizeRect.x - 1, minimizeRect.y,
|
|
minimizeRect.x - 1, minimizeRect.y + minimizeRect.height);
|
|
AColor::Line(*dc, minimizeRect.x, minimizeRect.y - 1,
|
|
minimizeRect.x + minimizeRect.width, minimizeRect.y - 1);
|
|
}
|
|
|
|
void TrackInfo::DrawBackground(wxDC * dc, const wxRect & r, bool bSelected,
|
|
bool WXUNUSED(bHasMuteSolo), const int labelw, const int WXUNUSED(vrul)) const
|
|
{
|
|
// fill in label
|
|
wxRect fill = r;
|
|
fill.width = labelw-4;
|
|
AColor::MediumTrackInfo(dc, bSelected);
|
|
dc->DrawRectangle(fill);
|
|
|
|
// Vaughan, 2010-09-16: No more bevels around controls area. Now only around buttons.
|
|
//if( bHasMuteSolo )
|
|
//{
|
|
// fill=wxRect( r.x+1, r.y+17, vrul-6, 32);
|
|
// AColor::BevelTrackInfo( *dc, true, fill );
|
|
//
|
|
// fill=wxRect( r.x+1, r.y+67, fill.width, r.height-87);
|
|
// AColor::BevelTrackInfo( *dc, true, fill );
|
|
//}
|
|
//else
|
|
//{
|
|
// fill=wxRect( r.x+1, r.y+17, vrul-6, r.height-37);
|
|
// AColor::BevelTrackInfo( *dc, true, fill );
|
|
//}
|
|
}
|
|
|
|
void TrackInfo::GetTrackControlsRect(const wxRect & r, wxRect & dest) const
|
|
{
|
|
wxRect top;
|
|
wxRect bot;
|
|
|
|
GetTitleBarRect(r, top);
|
|
GetMinimizeRect(r, bot);
|
|
|
|
dest.x = r.x;
|
|
dest.width = kTrackInfoWidth - dest.x;
|
|
dest.y = top.GetBottom() + 2; // BUG
|
|
dest.height = bot.GetTop() - top.GetBottom() - 2;
|
|
}
|
|
|
|
|
|
void TrackInfo::DrawCloseBox(wxDC * dc, const wxRect & r, bool down) const
|
|
{
|
|
wxRect bev;
|
|
GetCloseBoxRect(r, bev);
|
|
|
|
#ifdef EXPERIMENTAL_THEMING
|
|
wxPen pen( theTheme.Colour( clrTrackPanelText ));
|
|
dc->SetPen( pen );
|
|
#else
|
|
dc->SetPen(*wxBLACK_PEN);
|
|
#endif
|
|
|
|
// Draw the "X"
|
|
const int s = 6;
|
|
|
|
int ls = bev.x + ((bev.width - s) / 2);
|
|
int ts = bev.y + ((bev.height - s) / 2);
|
|
int rs = ls + s;
|
|
int bs = ts + s;
|
|
|
|
AColor::Line(*dc, ls, ts, rs, bs);
|
|
AColor::Line(*dc, ls + 1, ts, rs + 1, bs);
|
|
AColor::Line(*dc, rs, ts, ls, bs);
|
|
AColor::Line(*dc, rs + 1, ts, ls + 1, bs);
|
|
|
|
bev.Inflate(-1, -1);
|
|
AColor::BevelTrackInfo(*dc, !down, bev);
|
|
}
|
|
|
|
void TrackInfo::DrawTitleBar(wxDC * dc, const wxRect & r, Track * t,
|
|
bool down) const
|
|
{
|
|
wxRect bev;
|
|
GetTitleBarRect(r, bev);
|
|
bev.Inflate(-1, -1);
|
|
|
|
// Draw title text
|
|
SetTrackInfoFont(dc);
|
|
wxString titleStr = t->GetName();
|
|
int allowableWidth = kTrackInfoWidth - 38 - kLeftInset;
|
|
|
|
wxCoord textWidth, textHeight;
|
|
dc->GetTextExtent(titleStr, &textWidth, &textHeight);
|
|
while (textWidth > allowableWidth) {
|
|
titleStr = titleStr.Left(titleStr.Length() - 1);
|
|
dc->GetTextExtent(titleStr, &textWidth, &textHeight);
|
|
}
|
|
// wxGTK leaves little scraps (antialiasing?) of the
|
|
// characters if they are repeatedly drawn. This
|
|
// happens when holding down mouse button and moving
|
|
// in and out of the title bar. So clear it first.
|
|
AColor::MediumTrackInfo(dc, t->GetSelected());
|
|
dc->DrawRectangle(bev);
|
|
dc->DrawText(titleStr, r.x + kTrackInfoBtnSize + 3, r.y + 2);
|
|
|
|
// Pop-up triangle
|
|
#ifdef EXPERIMENTAL_THEMING
|
|
wxColour c = theTheme.Colour( clrTrackPanelText );
|
|
#else
|
|
wxColour c = *wxBLACK;
|
|
#endif
|
|
|
|
dc->SetPen(c);
|
|
dc->SetBrush(c);
|
|
|
|
int s = 10; // Width of dropdown arrow...height is half of width
|
|
AColor::Arrow(*dc,
|
|
bev.GetRight() - s - 3, // 3 to offset from right border
|
|
bev.y + ((bev.height - (s / 2)) / 2),
|
|
s);
|
|
|
|
AColor::BevelTrackInfo(*dc, !down, bev);
|
|
}
|
|
|
|
/// Draw the Mute or the Solo button, depending on the value of solo.
|
|
void TrackInfo::DrawMuteSolo(wxDC * dc, const wxRect & r, Track * t,
|
|
bool down, bool solo, bool bHasSoloButton) const
|
|
{
|
|
wxRect bev;
|
|
if( solo && !bHasSoloButton )
|
|
return;
|
|
GetMuteSoloRect(r, bev, solo, bHasSoloButton);
|
|
bev.Inflate(-1, -1);
|
|
|
|
if (bev.y + bev.height >= r.y + r.height - 19)
|
|
return; // don't draw mute and solo buttons, because they don't fit into track label
|
|
|
|
AColor::MediumTrackInfo( dc, t->GetSelected());
|
|
if( solo )
|
|
{
|
|
if( t->GetSolo() )
|
|
{
|
|
AColor::Solo(dc, t->GetSolo(), t->GetSelected());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if( t->GetMute() )
|
|
{
|
|
AColor::Mute(dc, t->GetMute(), t->GetSelected(), t->GetSolo());
|
|
}
|
|
}
|
|
//(solo) ? AColor::Solo(dc, t->GetSolo(), t->GetSelected()) :
|
|
// AColor::Mute(dc, t->GetMute(), t->GetSelected(), t->GetSolo());
|
|
dc->SetPen( *wxTRANSPARENT_PEN );//No border!
|
|
dc->DrawRectangle(bev);
|
|
|
|
wxCoord textWidth, textHeight;
|
|
wxString str = (solo) ?
|
|
/* i18n-hint: This is on a button that will silence this track.*/
|
|
_("Solo") :
|
|
/* i18n-hint: This is on a button that will silence all the other tracks.*/
|
|
_("Mute");
|
|
|
|
SetTrackInfoFont(dc);
|
|
dc->GetTextExtent(str, &textWidth, &textHeight);
|
|
dc->DrawText(str, bev.x + (bev.width - textWidth) / 2, bev.y);
|
|
|
|
AColor::BevelTrackInfo(*dc, (solo?t->GetSolo():t->GetMute()) == down, bev);
|
|
|
|
if (solo && !down) {
|
|
// Update the mute button, which may be grayed out depending on
|
|
// the state of the solo button.
|
|
DrawMuteSolo(dc, r, t, false, false, bHasSoloButton);
|
|
}
|
|
}
|
|
|
|
// Draw the minimize button *and* the sync-lock track icon, if necessary.
|
|
void TrackInfo::DrawMinimize(wxDC * dc, const wxRect & r, Track * t, bool down) const
|
|
{
|
|
wxRect bev;
|
|
GetMinimizeRect(r, bev);
|
|
|
|
// Clear background to get rid of previous arrow
|
|
AColor::MediumTrackInfo(dc, t->GetSelected());
|
|
dc->DrawRectangle(bev);
|
|
|
|
#ifdef EXPERIMENTAL_THEMING
|
|
wxColour c = theTheme.Colour(clrTrackPanelText);
|
|
dc->SetBrush(c);
|
|
dc->SetPen(c);
|
|
#else
|
|
AColor::Dark(dc, t->GetSelected());
|
|
#endif
|
|
|
|
AColor::Arrow(*dc,
|
|
bev.x - 5 + bev.width / 2,
|
|
bev.y - 2 + bev.height / 2,
|
|
10,
|
|
t->GetMinimized());
|
|
|
|
AColor::BevelTrackInfo(*dc, !down, bev);
|
|
}
|
|
|
|
#ifdef EXPERIMENTAL_MIDI_OUT
|
|
void TrackInfo::DrawVelocitySlider(wxDC *dc, NoteTrack *t, wxRect r) const
|
|
{
|
|
wxRect gainRect;
|
|
int index = t->GetIndex();
|
|
EnsureSufficientSliders(index);
|
|
GetGainRect(r, gainRect);
|
|
if (gainRect.y + gainRect.height < r.y + r.height - 19) {
|
|
mGains[index]->SetStyle(VEL_SLIDER);
|
|
GainSlider(index)->Move(wxPoint(gainRect.x, gainRect.y));
|
|
GainSlider(index)->Set(t->GetGain());
|
|
GainSlider(index)->OnPaint(*dc, t->GetSelected());
|
|
}
|
|
}
|
|
#endif
|
|
|
|
void TrackInfo::DrawSliders(wxDC *dc, WaveTrack *t, wxRect r) const
|
|
{
|
|
wxRect sliderRect;
|
|
|
|
GetGainRect(r, sliderRect);
|
|
if (sliderRect.y + sliderRect.height < r.y + r.height - 19) {
|
|
GainSlider(t)->OnPaint(*dc, t->GetSelected());
|
|
}
|
|
|
|
GetPanRect(r, sliderRect);
|
|
if (sliderRect.y + sliderRect.height < r.y + r.height - 19) {
|
|
PanSlider(t)->OnPaint(*dc, t->GetSelected());
|
|
}
|
|
}
|
|
|
|
LWSlider * TrackInfo::GainSlider(WaveTrack *t) const
|
|
{
|
|
wxRect r(kLeftInset, t->GetY() - pParent->GetViewInfo()->vpos + kTopInset, 1, t->GetHeight());
|
|
wxRect sliderRect;
|
|
GetGainRect(r, sliderRect);
|
|
|
|
mGain->Move(wxPoint(sliderRect.x, sliderRect.y));
|
|
mGain->Set(t->GetGain());
|
|
|
|
return mGain;
|
|
}
|
|
|
|
LWSlider * TrackInfo::PanSlider(WaveTrack *t) const
|
|
{
|
|
wxRect r(kLeftInset, t->GetY() - pParent->GetViewInfo()->vpos + kTopInset, 1, t->GetHeight());
|
|
wxRect sliderRect;
|
|
GetPanRect(r, sliderRect);
|
|
|
|
mPan->Move(wxPoint(sliderRect.x, sliderRect.y));
|
|
mPan->Set(t->GetPan());
|
|
|
|
return mPan;
|
|
}
|
|
|
|
static TrackPanel * TrackPanelFactory(wxWindow * parent,
|
|
wxWindowID id,
|
|
const wxPoint & pos,
|
|
const wxSize & size,
|
|
TrackList * tracks,
|
|
ViewInfo * viewInfo,
|
|
TrackPanelListener * listener,
|
|
AdornedRulerPanel * ruler)
|
|
{
|
|
return new TrackPanel(
|
|
parent,
|
|
id,
|
|
pos,
|
|
size,
|
|
tracks,
|
|
viewInfo,
|
|
listener,
|
|
ruler);
|
|
}
|
|
|
|
|
|
// Declare the static factory function.
|
|
// We defined it in the class.
|
|
TrackPanel *(*TrackPanel::FactoryFunction)(
|
|
wxWindow * parent,
|
|
wxWindowID id,
|
|
const wxPoint & pos,
|
|
const wxSize & size,
|
|
TrackList * tracks,
|
|
ViewInfo * viewInfo,
|
|
TrackPanelListener * listener,
|
|
AdornedRulerPanel * ruler) = TrackPanelFactory;
|
|
|