1
0
mirror of https://github.com/cookiengineer/audacity synced 2025-05-07 23:32:53 +02:00
audacity/src/TrackPanel.cpp
2017-06-15 08:21:37 -04:00

7776 lines
242 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 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 "TrackPanelCellIterator.h"
#include "TrackPanelMouseEvent.h"
//#define DEBUG_DRAW_TIMING 1
// #define SPECTRAL_EDITING_ESC_KEY
#include <wx/fontenum.h>
#include <wx/numdlg.h>
#include <wx/spinctrl.h>
#include "FreqWindow.h" // for SpectrumAnalyst
#include "AColor.h"
#include "AllThemeResources.h"
#include "AudioIO.h"
#include "float_cast.h"
#include "LabelTrack.h"
#include "MixerBoard.h"
#include "NoteTrack.h"
#include "NumberScale.h"
#include "Prefs.h"
#include "RefreshCode.h"
#include "ShuttleGui.h"
#include "TimeTrack.h"
#include "TrackArtist.h"
#include "TrackPanelAx.h"
#include "UndoManager.h"
#include "UIHandle.h"
#include "HitTestResult.h"
#include "WaveTrack.h"
#include "commands/Keyboard.h"
#include "ondemand/ODManager.h"
#include "prefs/PrefsDialog.h"
#include "prefs/SpectrumPrefs.h"
#include "prefs/TracksBehaviorsPrefs.h"
#include "prefs/WaveformPrefs.h"
#include "toolbars/ControlToolBar.h"
#include "toolbars/ToolsToolBar.h"
// To do: eliminate this!
#include "tracks/ui/Scrubbing.h"
#define ZOOMLIMIT 0.001f
//This loads the appropriate set of cursors, depending on platform.
#include "../images/Cursors.h"
DEFINE_EVENT_TYPE(EVT_TRACK_PANEL_TIMER)
/*
This is a diagram of TrackPanel's division of one (non-stereo) track rectangle.
Total height equals Track::GetHeight()'s value. Total width is the wxWindow's width.
Each charater that is not . represents one pixel.
Inset space of this track, and top inset of the next track, are used to draw the focus highlight.
Top inset of the right channel of a stereo track, and bottom shadow line of the
left channel, are used for the channel separator.
"Margin" is a term used for inset plus border (top and left) or inset plus
shadow plus border (right and bottom).
TrackInfo::GetTrackInfoWidth() == GetVRulerOffset()
counts columns from the left edge up to and including controls, and is a constant.
GetVRulerWidth() is variable -- all tracks have the same ruler width at any time,
but that width may be adjusted when tracks change their vertical scales.
GetLabelWidth() counts columns up to and including the VRuler.
GetLeftOffset() is yet one more -- it counts the "one pixel" column.
FindCell() for label returns a rectangle that OMITS left, top, and bottom
margins
FindCell() for vruler returns a rectangle right of the label,
up to and including the One Pixel column, and OMITS top and bottom margins
FindCell() for track returns a rectangle with x == GetLeftOffset(), and OMITS
right top, and bottom margins
+--------------- ... ------ ... --------------------- ... ... -------------+
| Top Inset |
| |
| +------------ ... ------ ... --------------------- ... ... ----------+ |
| L|+-Border---- ... ------ ... --------------------- ... ... -Border-+ |R |
| e||+---------- ... -++--- ... -+++----------------- ... ... -------+| |i |
| f|B| || ||| |BS|g |
| t|o| Controls || V |O| The good stuff |oh|h |
| |r| || R |n| |ra|t |
| I|d| || u |e| |dd| |
| n|e| || l | | |eo|I |
| s|r| || e |P| |rw|n |
| e||| || r |i| ||||s |
| t||| || |x| ||||e |
| ||| || |e| ||||t |
| ||| || |l| |||| |
| ||| || ||| |||| |
. ... .. ... .... .
. ... .. ... .... .
. ... .. ... .... .
| ||| || ||| |||| |
| ||+---------- -++-- ... -+++----------------- ... ... -------+||| |
| |+-Border---- ... ----- ... --------------------- ... ... -Border-+|| |
| | Shadow---- ... ----- ... --------------------- ... ... --Shadow-+| |
*/
// 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 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,
OnSetFontID,
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,
OnWaveformDBID,
OnSpectrumID,
OnSpectrogramSettingsID,
OnSplitStereoID,
OnSplitStereoMonoID,
OnMergeStereoID,
OnSwapChannelsID,
OnSetTimeTrackRangeID,
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, OverlayPanel)
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_KEY_UP(TrackPanel::OnKeyUp)
EVT_CHAR(TrackPanel::OnChar)
EVT_PAINT(TrackPanel::OnPaint)
EVT_SET_FOCUS(TrackPanel::OnSetFocus)
EVT_KILL_FOCUS(TrackPanel::OnKillFocus)
EVT_CONTEXT_MENU(TrackPanel::OnContextMenu)
EVT_MENU(OnSetFontID, TrackPanel::OnSetFont)
EVT_MENU(OnSetTimeTrackRangeID, TrackPanel::OnSetTimeTrackRange)
EVT_MENU_RANGE(OnUpOctaveID, OnDownOctaveID, TrackPanel::OnChangeOctave)
EVT_MENU_RANGE(OnChannelLeftID, OnChannelMonoID,
TrackPanel::OnChannelChange)
EVT_MENU_RANGE(OnWaveformID, OnSpectrumID, TrackPanel::OnSetDisplay)
EVT_MENU(OnSpectrogramSettingsID, TrackPanel::OnSpectrogramSettings)
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(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)
EVT_TIMER(wxID_ANY, TrackPanel::OnTimer)
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.
std::unique_ptr<wxCursor> MakeCursor( int WXUNUSED(CursorId), const char * pXpm[36], int HotX, int HotY )
{
#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.
Image.SetOption( wxIMAGE_OPTION_CUR_HOTSPOT_X, HotX-HotAdjust );
Image.SetOption( wxIMAGE_OPTION_CUR_HOTSPOT_Y, HotY-HotAdjust );
return std::make_unique<wxCursor>( Image );
}
// 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,
const std::shared_ptr<TrackList> &tracks,
ViewInfo * viewInfo,
TrackPanelListener * listener,
AdornedRulerPanel * ruler)
: OverlayPanel(parent, id, pos, size, wxWANTS_CHARS | wxNO_BORDER),
mTrackInfo(this),
mListener(listener),
mTracks(tracks),
mViewInfo(viewInfo),
mRuler(ruler),
mTrackArtist(nullptr),
mRefreshBacking(false),
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"));
SetBackgroundStyle(wxBG_STYLE_PAINT);
{
auto pAx = std::make_unique <TrackPanelAx>( this );
#if wxUSE_ACCESSIBILITY
// wxWidgets owns the accessible object
SetAccessible(mAx = pAx.release());
#else
// wxWidgets does not own the object, but we need to retain it
mAx = std::move(pAx);
#endif
}
mMouseCapture = IsUncaptured;
mLabelTrackStartXPos=-1;
mRedrawAfterStop = false;
mSelectCursor = MakeCursor( wxCURSOR_IBEAM, IBeamCursorXpm, 17, 16);
mEnvelopeCursor= MakeCursor( wxCURSOR_ARROW, EnvCursorXpm, 16, 16);
mDisabledCursor= MakeCursor( wxCURSOR_NO_ENTRY, DisabledCursorXpm,16, 16);
mZoomInCursor = MakeCursor( wxCURSOR_MAGNIFIER, ZoomInCursorXpm, 19, 15);
mZoomOutCursor = MakeCursor( wxCURSOR_MAGNIFIER, ZoomOutCursorXpm, 19, 15);
#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
#ifdef USE_MIDI
mStretchCursor = MakeCursor( wxCURSOR_BULLSEYE, StretchCursorXpm, 16, 16);
mStretchLeftCursor = MakeCursor( wxCURSOR_BULLSEYE,
StretchLeftCursorXpm, 16, 16);
mStretchRightCursor = MakeCursor( wxCURSOR_BULLSEYE,
StretchRightCursorXpm, 16, 16);
#endif
mArrowCursor = std::make_unique<wxCursor>(wxCURSOR_ARROW);
mResizeCursor = std::make_unique<wxCursor>(wxCURSOR_SIZENS);
mRearrangeCursor = std::make_unique<wxCursor>(wxCURSOR_HAND);
mAdjustLeftSelectionCursor = std::make_unique<wxCursor>(wxCURSOR_POINT_LEFT);
mAdjustRightSelectionCursor = std::make_unique<wxCursor>(wxCURSOR_POINT_RIGHT);
// Menu pointers are set to NULL here. Delay building of menus until after
// the command managter is finished, so that we can look up shortcut
// key strings that need to appear in some of the popup menus.
mWaveTrackMenu = NULL;
mChannelItemsInsertionPoint = 0;
mNoteTrackMenu = NULL;
mLabelTrackMenu = NULL;
mTimeTrackMenu = NULL;
mTrackArtist = std::make_unique<TrackArtist>();
mTrackArtist->SetMargins(1, kTopMargin, kRightMargin, kBottomMargin);
mCapturedTrack = NULL;
mPopupMenuTarget = NULL;
mTimeCount = 0;
mTimer.parent = this;
// Timer is started after the window is visible
GetProject()->Bind(wxEVT_IDLE, &TrackPanel::OnIdle, this);
mZoomStart = -1;
mZoomEnd = -1;
// This is used to snap the cursor to the nearest track that
// lines up with it.
mSnapManager = NULL;
// 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 = std::make_unique<SpectrumAnalyst>();
#endif
mSelStartValid = false;
mSelStart = 0;
}
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();
DeleteMenus();
}
LWSlider *TrackPanel::GainSlider( const WaveTrack *wt )
{
auto rect = FindTrackRect( wt, true );
wxRect sliderRect;
TrackInfo::GetGainRect( rect.GetTopLeft(), sliderRect );
return TrackInfo::GainSlider(sliderRect, wt, false, this);
}
LWSlider *TrackPanel::PanSlider( const WaveTrack *wt )
{
auto rect = FindTrackRect( wt, true );
wxRect sliderRect;
TrackInfo::GetPanRect( rect.GetTopLeft(), sliderRect );
return TrackInfo::PanSlider(sliderRect, wt, false, this);
}
#ifdef EXPERIMENTAL_MIDI_OUT
LWSlider *TrackPanel::VelocitySlider( const NoteTrack *nt )
{
auto rect = FindTrackRect( nt, true );
wxRect sliderRect;
TrackInfo::GetVelocityRect( rect.GetTopLeft(), sliderRect );
return TrackInfo::VelocitySlider(sliderRect, nt, false, this);
}
#endif
SelectionState &TrackPanel::GetSelectionState()
{
return GetProject()->GetSelectionState();
}
void TrackPanel::BuildMenusIfNeeded(void)
{
if (!mRateMenu)
BuildMenus();
}
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.
auto rateMenu = std::make_unique<wxMenu>();
rateMenu->AppendRadioItem(OnRate8ID, wxT("8000 Hz"));
rateMenu->AppendRadioItem(OnRate11ID, wxT("11025 Hz"));
rateMenu->AppendRadioItem(OnRate16ID, wxT("16000 Hz"));
rateMenu->AppendRadioItem(OnRate22ID, wxT("22050 Hz"));
rateMenu->AppendRadioItem(OnRate44ID, wxT("44100 Hz"));
rateMenu->AppendRadioItem(OnRate48ID, wxT("48000 Hz"));
rateMenu->AppendRadioItem(OnRate88ID, wxT("88200 Hz"));
rateMenu->AppendRadioItem(OnRate96ID, wxT("96000 Hz"));
rateMenu->AppendRadioItem(OnRate176ID, wxT("176400 Hz"));
rateMenu->AppendRadioItem(OnRate192ID, wxT("192000 Hz"));
rateMenu->AppendRadioItem(OnRate352ID, wxT("352800 Hz"));
rateMenu->AppendRadioItem(OnRate384ID, wxT("384000 Hz"));
rateMenu->AppendRadioItem(OnRateOtherID, _("&Other..."));
auto formatMenu = std::make_unique<wxMenu>();
formatMenu->AppendRadioItem(On16BitID, GetSampleFormatStr(int16Sample));
formatMenu->AppendRadioItem(On24BitID, GetSampleFormatStr(int24Sample));
formatMenu->AppendRadioItem(OnFloatID, GetSampleFormatStr(floatSample));
/* build the pop-down menu used on wave (sampled audio) tracks */
mWaveTrackMenu = std::make_unique<wxMenu>();
mWaveTrackMenu->AppendRadioItem(OnWaveformID, _("Wa&veform"));
mWaveTrackMenu->AppendRadioItem(OnWaveformDBID, _("&Waveform (dB)"));
mWaveTrackMenu->AppendRadioItem(OnSpectrumID, _("&Spectrogram"));
mWaveTrackMenu->Append(OnSpectrogramSettingsID, _("S&pectrogram Settings..."));
mWaveTrackMenu->AppendSeparator();
// include both mono and stereo items as a work around for bug 1250
// 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"));
// DA: Uses split stereo track and then drag pan sliders for split-stereo-to-mono
#ifndef EXPERIMENTAL_DA
mWaveTrackMenu->Append(OnSplitStereoMonoID, _("Split Stereo to Mo&no"));
#endif
mWaveTrackMenu->AppendSeparator();
mWaveTrackMenu->Append(0, _("&Format"), (mFormatMenu = formatMenu.release()));
mWaveTrackMenu->AppendSeparator();
mWaveTrackMenu->Append(0, _("Rat&e"), (mRateMenu = rateMenu.release()));
/* build the pop-down menu used on note (MIDI) tracks */
mNoteTrackMenu = std::make_unique<wxMenu>();
mNoteTrackMenu->Append(OnUpOctaveID, _("Up &Octave"));
mNoteTrackMenu->Append(OnDownOctaveID, _("Down Octa&ve"));
/* build the pop-down menu used on label tracks */
mLabelTrackMenu = std::make_unique<wxMenu>();
mLabelTrackMenu->Append(OnSetFontID, _("&Font..."));
/* build the pop-down menu used on time warping tracks */
mTimeTrackMenu = std::make_unique<wxMenu>();
mTimeTrackMenu->AppendRadioItem(OnTimeTrackLinID, wxT("&Linear scale"));
mTimeTrackMenu->AppendRadioItem(OnTimeTrackLogID, _("L&ogarithmic scale"));
mTimeTrackMenu->AppendSeparator();
mTimeTrackMenu->Append(OnSetTimeTrackRangeID, _("&Range..."));
mTimeTrackMenu->AppendCheckItem(OnTimeTrackLogIntID, _("Logarithmic &Interpolation"));
/*
mRulerWaveformMenu = std::make_unique<wxMenu>();
BuildVRulerMenuItems
(mRulerWaveformMenu.get(), OnFirstWaveformScaleID,
WaveformSettings::GetScaleNames());
mRulerSpectrumMenu = std::make_unique<wxMenu>();
BuildVRulerMenuItems
(mRulerSpectrumMenu.get(), OnFirstSpectrumScaleID,
SpectrogramSettings::GetScaleNames());
*/
}
/*
// left over from PRL's vertical ruler context menu experiment in 2.1.2
// 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
mRateMenu = mFormatMenu = nullptr;
mWaveTrackMenu.reset();
mNoteTrackMenu.reset();
mLabelTrackMenu.reset();
mTimeTrackMenu.reset();
mRulerWaveformMenu.reset();
mRulerSpectrumMenu.reset();
}
#ifdef EXPERIMENTAL_OUTPUT_DISPLAY
void TrackPanel::UpdateVirtualStereoOrder()
{
TrackListOfKindIterator iter(Track::Wave, GetTracks());
Track *t;
int temp;
for (t = iter.First(); t; t = iter.Next()) {
const auto wt = static_cast<WaveTrack*>(t);
if(t->GetChannel() == Track::MonoChannel){
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
wxString TrackPanel::gSoloPref;
void TrackPanel::UpdatePrefs()
{
gPrefs->Read(wxT("/GUI/AutoScroll"), &mViewInfo->bUpdateTrackIndicator,
true);
gPrefs->Read(wxT("/GUI/Solo"), &gSoloPref, wxT("Simple"));
#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();
mTrackInfo.UpdatePrefs();
Refresh();
}
void TrackPanel::ApplyUpdatedTheme()
{
mTrackInfo.ReCreateSliders();
}
/// 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;
}
/// Select all tracks marked by the label track lt
void TrackPanel::SelectTracksByLabel( LabelTrack *lt )
{
TrackListIterator iter(GetTracks());
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 )
{
GetSelectionState().SelectTrack( *mTracks, *t, true, true, GetMixerBoard() );
t = iter.Next();
}
}
void TrackPanel::GetTracksUsableArea(int *width, int *height) const
{
GetSize(width, height);
if (width) {
*width -= GetLeftOffset();
*width -= kRightMargin;
*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;
}
void TrackPanel::OnIdle(wxIdleEvent& event)
{
// The window must be ready when the timer fires (#1401)
if (IsShownOnScreen())
{
mTimer.Start(kTimerInterval, FALSE);
// Timer is started, we don't need the event anymore
GetProject()->Unbind(wxEVT_IDLE, &TrackPanel::OnIdle, this);
}
else
{
// Get another idle event, wx only guarantees we get one
// event after "some other normal events occur"
event.RequestMore();
}
}
/// AS: This gets called on our wx timer events.
void TrackPanel::OnTimer(wxTimerEvent& )
{
#ifdef __WXMAC__
// Unfortunate part of fix for bug 1431
// Without this, the toolbars hide only every other time that you press
// the yellow title bar button. For some reason, not every press sends
// us a deactivate event for the application.
{
auto project = GetProject();
if (project->IsIconized())
project->MacShowUndockedToolbars(false);
}
#endif
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();
}
AudacityProject *const p = GetProject();
// 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();
}
if (mLastDrawnSelectedRegion != mViewInfo->selectedRegion) {
UpdateSelectionDisplay();
}
// Notify listeners for timer ticks
{
wxCommandEvent e(EVT_TRACK_PANEL_TIMER);
p->GetEventHandler()->ProcessEvent(e);
}
DrawOverlays(false);
mRuler->DrawOverlays(false);
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();
}
else {
// Bug1387: enable autoscroll during drag, if the pointer is at either extreme x
// coordinate of the screen, even if that is still within the track area.
int xx = mMouseMostRecentX, yy = 0;
this->ClientToScreen(&xx, &yy);
if (xx == 0) {
mAutoScrolling = true;
mListener->TP_ScrollLeft();
}
else {
int width, height;
::wxDisplaySize(&width, &height);
if (xx == width - 1) {
mAutoScrolling = true;
mListener->TP_ScrollRight();
}
}
}
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;
}
}
double TrackPanel::GetScreenEndTime() const
{
int width;
GetTracksUsableArea(&width, NULL);
return mViewInfo->PositionToTime(width, 0, true);
}
/// AS: OnPaint( ) is called during the normal course of
/// completing a repaint operation.
void TrackPanel::OnPaint(wxPaintEvent & /* event */)
{
mLastDrawnSelectedRegion = mViewInfo->selectedRegion;
#if DEBUG_DRAW_TIMING
wxStopWatch sw;
#endif
{
wxPaintDC dc(this);
// Retrieve the damage rectangle
wxRect box = GetUpdateRegion().GetBox();
// 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;
// Redraw the backing bitmap
DrawTracks(&GetBackingDCForRepaint());
// Copy it to the display
DisplayBitmap(dc);
}
else
{
// Copy full, possibly clipped, damage rectangle
RepairBitmap(dc, box.x, box.y, box.width, box.height);
}
// Done with the clipped DC
// Drawing now goes directly to the client area.
// DrawOverlays() may need to draw outside the clipped region.
// (Used to make a NEW, separate wxClientDC, but that risks flashing
// problems on Mac.)
dc.DestroyClippingRegion();
DrawOverlays(true, &dc);
}
#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(const wxString &desc, const wxString &shortDesc,
UndoPush flags)
{
mListener->TP_PushState(desc, shortDesc, flags);
}
void TrackPanel::MakeParentPushState(const wxString &desc, const wxString &shortDesc)
{
MakeParentPushState(desc, shortDesc, UndoPush::AUTOSAVE);
}
void TrackPanel::MakeParentModifyState(bool bWantsAutoSave)
{
mListener->TP_ModifyState(bWantsAutoSave);
}
void TrackPanel::MakeParentRedrawScrollbars()
{
mListener->TP_RedrawScrollbars();
}
void TrackPanel::HandleInterruptedDrag()
{
bool sendEvent = true;
if (mUIHandle)
sendEvent = mUIHandle->StopsOnKeystroke();
// Certain drags need to complete their effects before handling keystroke shortcut
// commands: those that have undoable editing effects. For others, keystrokes are
// harmless and we do nothing.
else switch (mMouseCapture)
{
case IsUncaptured:
case IsVZooming:
case IsSelecting:
case IsSelectingLabelText:
case IsResizing:
case IsResizingBetweenLinkedTracks:
case IsResizingBelowLinkedTracks:
sendEvent = false;
default:
;
}
/*
So this includes the cases:
IsAdjustingLabel,
IsRearranging,
IsStretching
*/
if (sendEvent) {
// The bogus id isn't used anywhere, but may help with debugging.
// as this is sending a bogus mouse up. The mouse button is still actually down
// and may go up again.
const int idBogusUp = 2;
wxMouseEvent evt { wxEVT_LEFT_UP };
evt.SetId( idBogusUp );
evt.SetPosition(this->ScreenToClient(::wxGetMousePosition()));
this->ProcessEvent(evt);
}
}
namespace
{
void ProcessUIHandleResult
(TrackPanel *panel, AdornedRulerPanel *ruler,
Track *pClickedTrack, Track *pLatestTrack,
UIHandle::Result refreshResult)
{
// TODO: make a finer distinction between refreshing the track control area,
// and the waveform area. As it is, redraw both whenever you must redraw either.
using namespace RefreshCode;
panel->UpdateViewIfNoTracks();
if (refreshResult & DestroyedCell) {
// Beware stale pointer!
if (pLatestTrack == pClickedTrack)
pLatestTrack = NULL;
pClickedTrack = NULL;
}
if (pClickedTrack && (refreshResult & UpdateVRuler))
panel->UpdateVRuler(pClickedTrack);
if (refreshResult & DrawOverlays) {
panel->DrawOverlays(false);
ruler->DrawOverlays(false);
}
// Refresh all if told to do so, or if told to refresh a track that
// is not known.
const bool refreshAll =
( (refreshResult & RefreshAll)
|| ((refreshResult & RefreshCell) && !pClickedTrack)
|| ((refreshResult & RefreshLatestCell) && !pLatestTrack));
if (refreshAll)
panel->Refresh(false);
else {
if (refreshResult & RefreshCell)
panel->RefreshTrack(pClickedTrack);
if (refreshResult & RefreshLatestCell)
panel->RefreshTrack(pLatestTrack);
}
if (refreshResult & FixScrollbars)
panel->MakeParentRedrawScrollbars();
if (refreshResult & Resize)
panel->GetListener()->TP_HandleResize();
// This flag is superfluous if you do full refresh,
// because TrackPanel::Refresh() does this too
if (refreshResult & UpdateSelection) {
panel->DisplaySelection();
{
// Formerly in TrackPanel::UpdateSelectionDisplay():
// Make sure the ruler follows suit.
// mRuler->DrawSelection();
// ... but that too is superfluous it does nothing but refresh
// the ruler, while DisplaySelection calls TP_DisplaySelection which
// also always refreshes the ruler.
}
}
if ((refreshResult & EnsureVisible) && pClickedTrack)
panel->EnsureVisible(pClickedTrack);
}
}
void TrackPanel::CancelDragging()
{
if (mUIHandle) {
UIHandle::Result refreshResult = mUIHandle->Cancel(GetProject());
{
// TODO: avoid dangling pointers to mpClickedTrack
// when the undo stack management of the typical Cancel override
// causes it to relocate. That is implement some means to
// re-fetch the track according to its position in the list.
mpClickedTrack = NULL;
}
ProcessUIHandleResult(this, mRuler, mpClickedTrack, NULL, refreshResult);
mpClickedTrack = NULL;
mUIHandle = NULL;
if (HasCapture())
ReleaseMouse();
wxMouseEvent dummy;
HandleCursor(dummy);
}
}
bool TrackPanel::HandleEscapeKey(bool down)
{
if (!down)
return false;
if (mUIHandle) {
// UIHANDLE CANCEL
CancelDragging();
return true;
}
switch (mMouseCapture)
{
case IsSelecting:
{
mSelectionStateChanger.reset();
mViewInfo->selectedRegion = mInitialSelection;
// Refresh mixer board for change of set of selected tracks
if (MixerBoard* pMixerBoard = this->GetMixerBoard())
pMixerBoard->Refresh();
}
break;
case IsVZooming:
break;
case IsResizing:
mCapturedTrack->SetHeight(mInitialActualHeight);
mCapturedTrack->SetMinimized(mInitialMinimized);
break;
case IsResizingBetweenLinkedTracks:
{
Track *const next = mTracks->GetNext(mCapturedTrack);
mCapturedTrack->SetHeight(mInitialUpperActualHeight);
mCapturedTrack->SetMinimized(mInitialMinimized);
#ifdef EXPERIMENTAL_OUTPUT_DISPLAY
if( !MONO_WAVE_PAN(mCapturedTrack) )
#endif
{
next->SetHeight(mInitialActualHeight);
next->SetMinimized(mInitialMinimized);
}
}
break;
case IsResizingBelowLinkedTracks:
{
Track *const prev = mTracks->GetPrev(mCapturedTrack);
mCapturedTrack->SetHeight(mInitialActualHeight);
mCapturedTrack->SetMinimized(mInitialMinimized);
#ifdef EXPERIMENTAL_OUTPUT_DISPLAY
if( !MONO_WAVE_PAN(mCapturedTrack) )
#endif
{
prev->SetHeight(mInitialUpperActualHeight);
prev->SetMinimized(mInitialMinimized);
}
}
break;
default:
{
// Not escaping from a mouse drag
return false;
}
}
// Common part in all cases that escape from a drag
SetCapturedTrack(NULL, IsUncaptured);
if (HasCapture())
ReleaseMouse();
wxMouseEvent dummy;
HandleCursor(dummy);
Refresh(false);
return true;
}
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(2 * mViewInfo->h - GetScreenEndTime());
}
void TrackPanel::HandlePageDownKey()
{
mListener->TP_ScrollWindow(GetScreenEndTime());
}
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 IsRearranging:
SetCursor( unsafe ? *mDisabledCursor : *mRearrangeCursor);
return true;
case IsAdjustingLabel:
case IsSelectingLabelText:
return true;
#if 0
case IsStretching:
SetCursor( unsafe
? *mDisabledCursor
: *ChooseStretchCursor( mStretchState.mMode ) );
return true;
#endif
default:
break;
}
return false;
}
#if defined(__WXMAC__)
/* i18n-hint: Command names a modifier key on Macintosh keyboards */
#define CTRL_CLICK _("Command-Click")
#else
/* i18n-hint: Ctrl names a modifier key on Windows or Linux keyboards */
#define CTRL_CLICK _("Ctrl-Click")
#endif
/// 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,
const 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 if (event.m_x >= GetVRulerOffset() ){
// In VRuler but probably in a label track, and clicks don't do anything here, so no tip.
// Use a space for the tip, otherwsie we get he default message.
// TODO: Maybe the code for label tracks SHOULD treat the VRuler as part of the TrackInfo?
tip = wxT(" ");
SetCursor( *mArrowCursor );
}
else if( GetTrackCount() > 1 ){
// Set a status message if over TrackInfo.
//tip = _("Drag the track vertically to change the order of the tracks.");
// i18n-hint: %s is replaced by (translation of) 'Ctrl-Click' on windows, 'Command-Click' on Mac
tip = wxString::Format( _("%s to select or deselect track. Drag up or down to change track order."),
CTRL_CLICK );
SetCursor( *mArrowCursor );
}
else {
// Set a status message if over TrackInfo.
// i18n-hint: %s is replaced by (translation of) 'Ctrl-Click' on windows, 'Command-Click' on Mac
tip = wxString::Format( _("%s to select or deselect track."),
CTRL_CLICK );
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,
const 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.get();
}
void TrackPanel::HandleCenterFrequencyClick
(bool shiftDown, const WaveTrack *wt, double value)
{
if (shiftDown) {
// Disable time selection
mSelStartValid = false;
mFreqSelTrack = wt;
mFreqSelPin = value;
mFreqSelMode = FREQ_SEL_DRAG_CENTER;
}
else {
#ifndef SPECTRAL_EDITING_ESC_KEY
// Start center snapping
// 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,
const wxMouseEvent & event, const wxRect &rect, bool bMultiToolMode,
wxString &tip, const wxCursor ** ppCursor )
{
// Do not set the default cursor here and re-set later, that causes
// flashing.
*ppCursor = mSelectCursor.get();
//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() || !mViewInfo->bAdjustSelectionEdges)
{
MaySetOnDemandTip( t, tip );
return;
}
{
wxInt64 leftSel = mViewInfo->TimeToPosition(mViewInfo->selectedRegion.t0(), rect.x);
wxInt64 rightSel = mViewInfo->TimeToPosition(mViewInfo->selectedRegion.t1(), rect.x);
// Something is wrong if right edge comes before left edge
wxASSERT(!(rightSel < leftSel));
}
const bool bShiftDown = event.ShiftDown();
const bool bCtrlDown = event.ControlDown();
const bool bModifierDown = bShiftDown || bCtrlDown;
#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.get();
return;
}
#endif
// If not shift-down and not snapping center, then
// choose boundaries only in snapping tolerance,
// and may choose center.
SelectionBoundary boundary =
ChooseBoundary(event, t, rect, !bModifierDown, !bModifierDown);
#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 ( auto stretchMode = HitTestStretch( t, rect, event ) ) {
tip = _("Click and drag to stretch within selected region.");
*ppCursor = ChooseStretchCursor( stretchMode );
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.get();
return;
case SBRight:
tip = _("Click and drag to move right selection boundary.");
*ppCursor = mAdjustRightSelectionCursor.get();
return;
#ifdef EXPERIMENTAL_SPECTRAL_EDITING
case SBBottom:
tip = _("Click and drag to move bottom selection frequency.");
*ppCursor = mBottomFrequencyCursor.get();
return;
case SBTop:
tip = _("Click and drag to move top selection frequency.");
*ppCursor = mTopFrequencyCursor.get();
return;
case SBCenter:
HandleCenterFrequencyCursor(bShiftDown, tip, ppCursor);
return;
case SBWidth:
tip = _("Click and drag to adjust frequency bandwidth.");
*ppCursor = mBandWidthCursor.get();
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,
const wxMouseEvent &, wxString& )
{
// Change the cursor based on the active tool.
switch (tool) {
case selectTool:
wxFAIL;// should have already been handled
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,
const auto foundCell = FindCell( event.m_x, event.m_y );
auto &track = foundCell.pTrack;
auto &rect = foundCell.rect;
auto &pCell = foundCell.pCell;
wxCursor *pCursor = NULL;
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?
// (Add margin back to bottom of the rectangle)
if (within(event.m_y,
(rect.GetBottom() + (kBottomMargin + kTopMargin) / 2),
TRACK_RESIZE_REGION))
SetCursorAndTipWhenInVResizeArea(
track->GetLinked() && foundCell.type != CellType::Label, tip);
// tip may still be NULL at this point, in which case we go on looking.
if (pCell && pCursor == NULL && tip == wxString()) {
const auto size = GetSize();
HitTestResult hitTest(pCell->HitTest
(TrackPanelMouseEvent{ event, rect, size, pCell }, GetProject()));
tip = hitTest.preview.message;
ProcessUIHandleResult(this, mRuler, track, track, hitTest.preview.refreshCode);
pCursor = hitTest.preview.cursor;
if (pCursor)
SetCursor(*pCursor);
else if (foundCell.type == CellType::Label ||
foundCell.type == CellType::VRuler)
SetCursorAndTipWhenInLabel(track, event, tip);
}
// Is it a label track?
if (track &&
pCursor == NULL && 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( pCursor == NULL && 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);
if( tool != selectTool )
{
// We don't include the select tool in
// SetCursorAndTipByTool() because it's more complex than
// the other tool cases.
SetCursorAndTipByTool( tool, event, tip);
}
else
{
bool bMultiToolMode = ttb->IsDown(multiTool);
const wxCursor *pSelection = 0;
SetCursorAndTipWhenSelectTool
( track, event, rect, bMultiToolMode, tip, &pSelection );
if (pSelection)
// Set cursor once only here, to avoid flashing during drags
SetCursor(*pSelection);
}
}
if (pCursor != NULL || 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)
{
const auto foundCell = FindCell(event.m_x, event.m_y);
auto &t = foundCell.pTrack;
auto &rect = foundCell.rect;
// 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, rect);
} else if (event.LeftUp() || event.RightUp()) {
mSnapManager.reset();
if (mSelectionStateChanger) {
mSelectionStateChanger->Commit();
mSelectionStateChanger.reset();
}
#ifdef USE_MIDI
bool left;
if ( GetProject()->IsSyncLocked() &&
( ( left = mStretchState.mMode == stretchLeft ) ||
mStretchState.mMode == stretchRight ) ) {
auto pNt = static_cast< NoteTrack * >( mCapturedTrack );
SyncLockedTracksIterator syncIter( GetTracks() );
for ( auto track = syncIter.StartWith(pNt); track != nullptr;
track = syncIter.Next() ) {
if ( track != pNt ) {
if ( left ) {
auto origT0 = mStretchState.mOrigT0;
auto diff = mViewInfo->selectedRegion.t0() - origT0;
if ( diff > 0)
track->SyncLockAdjust( origT0 + diff, origT0 );
else
track->SyncLockAdjust( origT0, origT0 - diff );
track->Offset( diff );
}
else {
auto origT1 = mStretchState.mOrigT1;
auto diff = mViewInfo->selectedRegion.t1() - origT1;
track->SyncLockAdjust( origT1, origT1 + diff );
}
}
}
}
if ( mStretchState.mStretching ) {
MakeParentPushState(_("Stretch Note Track"), _("Stretch"),
UndoPush::CONSOLIDATE | UndoPush::AUTOSAVE);
Refresh(false);
SetCapturedTrack( NULL );
}
else
#endif
{
// Do not draw yellow lines
if (mSnapLeft != -1 || mSnapRight != -1) {
mSnapLeft = mSnapRight = -1;
Refresh(false);
}
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()) {
if (!mCapturedTrack) {
mCapturedTrack = t;
if (!mCapturedTrack)
return;
}
// Deselect all other tracks and select this one.
GetSelectionState().SelectNone( *mTracks, GetMixerBoard() );
GetSelectionState().SelectTrack
( *mTracks, *mCapturedTrack, true, true, GetMixerBoard() );
// Default behavior: select whole track
SelectionState::SelectTrackLength
( *mTracks, *mViewInfo, *mCapturedTrack, GetProject()->IsSyncLocked() );
// 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());
}
Refresh(false);
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, rect.y, rect.height, t);
#endif
#endif
done:
SelectionHandleDrag(event, t);
}
// Counts tracks, counting stereo tracks as one track.
size_t TrackPanel::GetTrackCount(){
size_t count = 0;
TrackListIterator iter(GetTracks());
for (Track *t = iter.First(); t; t = iter.Next()) {
count += 1;
if( t->GetLinked() ){
t = iter.Next();
if( !t )
break;
}
}
return count;
}
// Counts selected tracks, counting stereo tracks as one track.
size_t TrackPanel::GetSelectedTrackCount(){
size_t count = 0;
TrackListIterator iter(GetTracks());
for (Track *t = iter.First(); t; t = iter.Next()) {
count += t->GetSelected() ? 1:0;
if( t->GetLinked() ){
t = iter.Next();
if( !t )
break;
}
}
return count;
}
/// This method gets called when we're handling selection
/// and the mouse was just clicked.
void TrackPanel::SelectionHandleClick(wxMouseEvent & event,
Track * pTrack, wxRect rect)
{
mCapturedTrack = pTrack;
rect.y += kTopMargin;
rect.height -= kTopMargin + kBottomMargin;
mCapturedRect = rect;
mMouseCapture=IsSelecting;
mInitialSelection = mViewInfo->selectedRegion;
mSelectionStateChanger = std::make_unique< SelectionStateChanger >
( GetSelectionState(), *mTracks );
// We create a NEW snap manager in case any snap-points have changed
mSnapManager = std::make_unique<SnapManager>(GetTracks(), mViewInfo);
mSnapLeft = mSnapRight = -1;
#ifdef USE_MIDI
mStretchState = StretchState{};
auto stretch = HitTestStretch( pTrack, rect, event, &mStretchState );
#endif
bool bShiftDown = event.ShiftDown();
bool bCtrlDown = event.ControlDown();
if (bShiftDown || (bCtrlDown
#ifdef USE_MIDI
&& !stretch
#endif
)) {
if( bShiftDown )
GetSelectionState().ChangeSelectionOnShiftClick
( *mTracks, *pTrack, GetMixerBoard() );
if( bCtrlDown ){
//Commented out bIsSelected toggles, as in Track Control Panel.
//bool bIsSelected = pTrack->GetSelected();
//Actual bIsSelected will always add.
bool bIsSelected = false;
// Don't toggle away the last selected track.
if( !bIsSelected || GetSelectedTrackCount() > 1 )
GetSelectionState().SelectTrack
( *mTracks, *pTrack, !bIsSelected, true, GetMixerBoard() );
}
double value;
// Shift-click, choose closest boundary
SelectionBoundary boundary =
ChooseBoundary(event, pTrack, rect, 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, rect.x, pTrack);
break;
}
#ifdef EXPERIMENTAL_SPECTRAL_EDITING
case SBBottom:
case SBTop:
{
// Reach this case only for wave tracks
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, rect.y, rect.height);
break;
}
case SBCenter:
HandleCenterFrequencyClick(true,
// Reach this case only for wave tracks
static_cast<const WaveTrack *>(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
) {
// Used to jump the play head, but it is redundant with timeline quick play
// StartOrJumpPlayback(event);
// 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 (mViewInfo->bAdjustSelectionEdges) {
#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, rect.y, rect.height);
UpdateSelectionDisplay();
// Frequency selection persists too, so do this:
MakeParentModifyState(false);
return;
}
else
#endif
{
// Not shift-down, choose boundary only within snapping
double value;
SelectionBoundary boundary =
ChooseBoundary(event, pTrack, rect, 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;
// Reach this case only for wave tracks
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,
// Reach this case only for wave track
static_cast<const WaveTrack *>(pTrack), value);
startNewSelection = false;
break;
#endif
default:
wxASSERT(false);
}
}
} // mViewInfo->bAdjustSelectionEdges
}
#ifdef USE_MIDI
if (stretch) {
// stretch is true only when pTrack is note
const auto nt = static_cast<NoteTrack *>(pTrack);
// find nearest beat to sel0, sel1
double minPeriod = 0.05; // minimum beat period
// If there is not (almost) a beat to stretch that is slower
// than 20 beats per second, don't stretch
if ( within( mStretchState.mBeat0.second,
mStretchState.mBeat1.second, 0.9 ) ||
( mStretchState.mBeat1.first - mStretchState.mBeat0.first ) /
( mStretchState.mBeat1.second - mStretchState.mBeat0.second )
< minPeriod )
return;
if (startNewSelection) { // mouse is not at an edge, but after
// quantization, we could be indicating the selection edge
if ( stretch == stretchLeft ) {
mListener->TP_DisplayStatusMessage(
_("Click and drag to stretch selected region."));
SetCursor(*mStretchLeftCursor);
// mStretchMode = stretchLeft;
startNewSelection = false;
}
else if ( stretchRight ) {
mListener->TP_DisplayStatusMessage(
_("Click and drag to stretch selected region."));
SetCursor(*mStretchRightCursor);
// mStretchMode = stretchRight;
startNewSelection = false;
}
}
mStretchState.mMode = stretch;
if ( mStretchState.mMode == stretchCenter ) {
mStretchState.mLeftBeats =
mStretchState.mBeat1.second - mStretchState.mBeatCenter.second;
mStretchState.mRightBeats =
mStretchState.mBeatCenter.second - mStretchState.mBeat0.second;
}
else if (mStretchState.mMode == stretchLeft) {
mStretchState.mLeftBeats = 0;
mStretchState.mRightBeats =
mStretchState.mBeat1.second - mStretchState.mBeat0.second;
} else if (mStretchState.mMode == stretchRight) {
mStretchState.mLeftBeats =
mStretchState.mBeat1.second - mStretchState.mBeat0.second;
mStretchState.mRightBeats = 0;
}
// Do this before we change the selection
MakeParentModifyState( false );
mViewInfo->selectedRegion.setTimes
( mStretchState.mBeat0.first, mStretchState.mBeat1.first );
mStretchState.mStretching = true;
// 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
GetSelectionState().SelectNone( *mTracks, GetMixerBoard() );
#ifdef EXPERIMENTAL_SPECTRAL_EDITING
StartFreqSelection (event.m_y, rect.y, rect.height, pTrack);
#endif
StartSelection(event.m_x, rect.x);
GetSelectionState().SelectTrack
( *mTracks, *pTrack, true, true, GetMixerBoard() );
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 = 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 = 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 = 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();
}
void TrackPanel::UpdateAccessibility()
{
if (mAx)
mAx->Updated();
}
void TrackPanel::MessageForScreenReader(const wxString& message)
{
if (mAx)
mAx->MessageForScreenReader(message);
}
#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 auto 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 size_t minLength = 8;
const double rate = pTrack->GetRate();
// Grab samples, just for this track, at these times
std::vector<float> frequencySnappingData;
const auto start =
pTrack->TimeToLongSamples(mViewInfo->selectedRegion.t0());
const auto end =
pTrack->TimeToLongSamples(mViewInfo->selectedRegion.t1());
const auto length =
std::min(frequencySnappingData.max_size(),
limitSampleBufferSize( 10485760, // as in FreqWindow.cpp
end - start ));
const auto effectiveLength = std::max(minLength, length);
frequencySnappingData.resize(effectiveLength, 0.0f);
pTrack->Get(
reinterpret_cast<samplePtr>(&frequencySnappingData[0]),
floatSample, start, length, fillZero,
// Don't try to cope with exceptions, just read zeroes instead.
false);
// 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();
auto 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)) {
// Spectral selection track is always wave
const auto wt = static_cast<const 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();
// SelectTrack(pTrack, true);
SetFocusedTrack(pTrack);
}
}
void TrackPanel::StartFreqSelection (int mouseYCoordinate, int trackTopEdge,
int trackHeight, Track *pTrack)
{
mFreqSelTrack = NULL;
mFreqSelMode = FREQ_SEL_INVALID;
mFreqSelPin = SelectedRegion::UndefinedFrequency;
if (isSpectralSelectionTrack(pTrack)) {
// Spectral selection track is always wave
mFreqSelTrack = static_cast<const 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::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 (std::abs (logHint - logf1) < std::abs (logHint - logf0))
mFreqSelPin = f0;
else
mFreqSelPin = f1;
}
}
else {
if (f1 < 0 ||
std::abs (hintFrequency - f1) < std::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
wxCursor *TrackPanel::ChooseStretchCursor( StretchEnum mode )
{
switch ( mode ) {
case stretchCenter: return mStretchCursor.get();
case stretchLeft: return mStretchLeftCursor.get();
case stretchRight: return mStretchRightCursor.get();
default: return nullptr;
}
}
auto TrackPanel::ChooseStretchMode
( const wxMouseEvent &event, const wxRect &rect, const ViewInfo &viewInfo,
const NoteTrack *nt, StretchState *pState ) -> StretchEnum
{
// Assume x coordinate is in the selection and y is appropriate for stretch
// -- and then decide whether x is near enough to either edge or neither.
Maybe< StretchState > state;
if ( !pState )
state.create(), pState = state.get();
if ( nt ) {
pState->mBeat0 =
nt->NearestBeatTime( viewInfo.selectedRegion.t0() );
pState->mOrigT0 = pState->mBeat0.first;
pState->mBeat1 =
nt->NearestBeatTime( viewInfo.selectedRegion.t1() );
pState->mOrigT1 = pState->mBeat1.first;
auto selStart = viewInfo.PositionToTime(event.m_x, rect.x);
pState->mBeatCenter = nt->NearestBeatTime( selStart );
if ( within( pState->mBeat0.second, pState->mBeatCenter.second, 0.1 ) )
return stretchLeft;
else if ( within( pState->mBeat1.second, pState->mBeatCenter.second, 0.1 ) )
return stretchRight;
}
else {
pState->mBeat0 = pState->mBeat1 = pState->mBeatCenter = { 0, 0 };
return stretchNone;
}
return stretchCenter;
}
void TrackPanel::Stretch(int mouseXCoordinate, int trackLeftEdge,
Track *pTrack)
{
if (pTrack == NULL && mCapturedTrack != NULL)
pTrack = mCapturedTrack;
if (!pTrack || pTrack->GetKind() != Track::Note) {
return;
}
NoteTrack *pNt = static_cast< NoteTrack * >( pTrack );
double moveto =
std::max(0.0, mViewInfo->PositionToTime(mouseXCoordinate, trackLeftEdge));
auto t1 = mViewInfo->selectedRegion.t1();
auto t0 = mViewInfo->selectedRegion.t0();
double dur, left_dur, right_dur;
// check to make sure tempo is not higher than 20 beats per second
// (In principle, tempo can be higher, but not infinity.)
const double minPeriod = 0.05; // minimum beat period
// make sure target duration is not too short
// Take quick exit if so, without changing the selection.
switch (mStretchState.mMode) {
case stretchLeft: {
dur = t1 - moveto;
if (dur < mStretchState.mRightBeats * minPeriod)
return;
pNt->StretchRegion
( mStretchState.mBeat0, mStretchState.mBeat1, dur );
mStretchState.mBeat0.first = moveto;
pNt->Offset( moveto - t0 );
mViewInfo->selectedRegion.setT0(moveto);
break;
}
case stretchRight: {
dur = moveto - t0;
if (dur < mStretchState.mLeftBeats * minPeriod)
return;
pNt->StretchRegion
( mStretchState.mBeat0, mStretchState.mBeat1, dur );
mViewInfo->selectedRegion.setT1(moveto);
mStretchState.mBeat1.first = moveto;
break;
}
case stretchCenter: {
left_dur = moveto - t0;
right_dur = t1 - moveto;
if (left_dur < mStretchState.mLeftBeats * minPeriod ||
right_dur < mStretchState.mRightBeats * minPeriod)
return;
pNt->StretchRegion
( mStretchState.mBeatCenter, mStretchState.mBeat1, right_dur );
pNt->StretchRegion
( mStretchState.mBeat0, mStretchState.mBeatCenter, left_dur );
mStretchState.mBeatCenter.first = moveto;
break;
}
default:
wxASSERT(false);
break;
}
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)
{
// 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
// JKC YES it has meaning.
//return;
}
wxRect rect = mCapturedRect;
Track *pTrack = mCapturedTrack;
// 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, rect.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
&& !mStretchState.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;
Track *eTrack = FindCell(x, y).pTrack;
if( !event.ControlDown() && sTrack && eTrack )
GetSelectionState().SelectRangeOfTracks
( *mTracks, *sTrack, *eTrack, GetMixerBoard() );
#ifdef USE_MIDI
if (mStretchState.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, rect.x, clickedTrack);
return;
}
#endif
#ifdef EXPERIMENTAL_SPECTRAL_EDITING
#ifndef SPECTRAL_EDITING_ESC_KEY
if (mFreqSelMode == FREQ_SEL_SNAPPING_CENTER &&
!mViewInfo->selectedRegion.isPoint())
MoveSnappingFreqSelection(y, rect.y, rect.height, pTrack);
else
#endif
if (mFreqSelTrack == pTrack)
ExtendFreqSelection(y, rect.y, rect.height);
#endif
ExtendSelection(x, rect.x, clickedTrack);
// If scrubbing does not use the helper poller thread, then
// don't do this at every mouse event, because it slows down seek-scrub.
// Instead, let OnTimer do it, which is often enough.
// And even if scrubbing does use the thread, then skipping this does not
// bring that advantage, but it is probably still a good idea anyway.
// 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();
float minFreq, maxFreq;
wt->GetSpectrumBounds(&minFreq, &maxFreq);
const NumberScale numberScale( settings.GetScale( minFreq, maxFreq ) );
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();
float minFreq, maxFreq;
wt->GetSpectrumBounds(&minFreq, &maxFreq);
const NumberScale numberScale( settings.GetScale( minFreq, maxFreq ) );
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 = std::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 = std::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
(const 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)) {
// Spectral selection track is always wave
const auto 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 = std::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;
}
}
/// Determines if drag zooming is active
bool TrackPanel::IsDragZooming(int zoomStart, int zoomEnd)
{
return (abs(zoomEnd - zoomStart) > DragThreshold);
}
/// Determines if the a modal tool is active
bool TrackPanel::IsMouseCaptured()
{
return (mMouseCapture != IsUncaptured || mCapturedTrack != NULL
|| mUIHandle != NULL);
}
/// 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;
const auto foundCell = FindCell(event.m_x, event.m_y);
mCapturedTrack = foundCell.pTrack;
mCapturedRect = foundCell.rect;
if (foundCell.type != CellType::VRuler || !(mCapturedTrack = foundCell.pTrack))
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;
const wxRect rect{
GetLeftOffset(),
mCapturedTrack->GetY() + kTopMargin,
GetSize().GetWidth() - GetLeftOffset(),
mCapturedTrack->GetHeight() - (kTopMargin + kBottomMargin)
};
if (IsDragZooming()) {
nt->ZoomTo(rect, mZoomStart, mZoomEnd);
} else if (event.ShiftDown() || event.RightUp()) {
nt->ZoomOut(rect, mZoomEnd);
} else {
nt->ZoomIn(rect, 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)
{
HandleWaveTrackVZoom(GetTracks(), mCapturedRect, mZoomStart, mZoomEnd,
track, shiftDown, rightUp, false);
mZoomEnd = mZoomStart = 0;
UpdateVRuler(track);
Refresh(false);
MakeParentModifyState(true);
}
//static
void TrackPanel::HandleWaveTrackVZoom
(TrackList *tracks, const wxRect &rect,
int zoomStart, int zoomEnd,
WaveTrack *track, bool shiftDown, bool rightUp,
bool fixedMousePoint)
{
// Assume linked track is wave or null
const auto partner = static_cast<WaveTrack *>(track->GetLink());
int height = track->GetHeight() - (kTopMargin + kBottomMargin);
int ypos = rect.y;
// Ensure start and end are in order (swap if not).
if (zoomEnd < zoomStart)
std::swap(zoomStart, zoomEnd);
float min, max, minBand = 0;
const double rate = track->GetRate();
const float halfrate = rate / 2;
const SpectrogramSettings &settings = track->GetSpectrogramSettings();
NumberScale scale;
const bool spectral = (track->GetDisplay() == WaveTrack::Spectrum);
const bool spectrumLinear = spectral &&
(track->GetSpectrogramSettings().scaleType == SpectrogramSettings::stLinear);
if (spectral) {
track->GetSpectrumBounds(&min, &max);
scale = settings.GetScale( min, max );
const auto fftLength = settings.GetFFTLength();
const float binSize = rate / fftLength;
// JKC: Following discussions of Bug 1208 I'm allowing zooming in
// down to one bin.
// const int minBins =
// std::min(10, fftLength / 2); //minimum 10 freq bins, unless there are less
const int minBins = 1;
minBand = minBins * binSize;
}
else
track->GetDisplayBounds(&min, &max);
if (IsDragZooming(zoomStart, zoomEnd)) {
// Drag Zoom
const float tmin = min, tmax = max;
if (spectral) {
double xmin = 1 - (zoomEnd - ypos) / (float)height;
double xmax = 1 - (zoomStart - 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 = (zoomStart - ypos) / (float)height;
const float p2 = (zoomEnd - 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...
const float 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
const float p1 = (zoomStart - ypos) / (float)height;
// (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 - p1;
if (fixedMousePoint) {
min = std::max(spectrumLinear ? 0.0f : 1.0f, scale.PositionToValue(-middle));
max = std::min(halfrate, scale.PositionToValue(1.0f + p1));
}
else {
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
const WaveformSettings &settings = track->GetWaveformSettings();
const bool linear = settings.isLinear();
const float top = linear
? 2.0
: (LINEAR_TO_DB(2.0) + settings.dBRange) / settings.dBRange;
if (min <= -1.0 && max >= 1.0) {
// Go to the maximal zoom-out
min = -top;
max = top;
}
else {
// limit to +/- 1 range unless already outside that range...
float minRange = (min < -1) ? -top : -1.0;
float maxRange = (max > 1) ? top : 1.0;
// and enforce vertical zoom limits.
const float p1 = (zoomStart - ypos) / (float)height;
if (fixedMousePoint) {
const float oldRange = max - min;
const float c = (max * (1.0 - p1) + min * p1);
min = std::min(maxRange - ZOOMLIMIT,
std::max(minRange, c - 2 * (1.0f - p1) * oldRange));
max = std::max(minRange + ZOOMLIMIT,
std::min(maxRange, c + 2 * p1 * oldRange));
}
else {
const float c = p1 * min + (1 - p1) * max;
const float l = (max - min);
min = std::min(maxRange - ZOOMLIMIT,
std::max(minRange, c - l));
max = std::max(minRange + ZOOMLIMIT,
std::min(maxRange, c + l));
}
}
}
}
}
else {
// Zoom IN
if (spectral) {
// Center the zoom-in at the click
const float p1 = (zoomStart - ypos) / (float)height;
const float middle = 1.0f - p1;
const float middleValue = scale.PositionToValue(middle);
if (fixedMousePoint) {
min = std::max(spectrumLinear ? 0.0f : 1.0f,
std::min(middleValue - minBand * middle,
scale.PositionToValue(0.5f * middle)
));
max = std::min(halfrate,
std::max(middleValue + minBand * p1,
scale.PositionToValue(middle + 0.5f * p1)
));
}
else {
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
if (min < -1.0 || max > 1.0) {
min = -1.0;
max = 1.0;
}
else {
// Enforce maximum vertical zoom
const float oldRange = max - min;
const float l = std::max(ZOOMLIMIT, 0.5f * oldRange);
const float ratio = l / (max - min);
const float p1 = (zoomStart - ypos) / (float)height;
const float c = (max * (1.0 - p1) + min * p1);
if (fixedMousePoint)
min = c - ratio * (1.0f - p1) * oldRange,
max = c + ratio * p1 * oldRange;
else
min = c - 0.5 * l,
max = c + 0.5 * l;
}
}
}
if (spectral) {
track->SetSpectrumBounds(min, max);
if (partner)
partner->SetSpectrumBounds(min, max);
}
else {
track->SetDisplayBounds(min, max);
if (partner)
partner->SetDisplayBounds(min, max);
}
}
void TrackPanel::UpdateViewIfNoTracks()
{
if (mTracks->IsEmpty())
{
// Be sure not to keep a dangling pointer
SetCapturedTrack(NULL);
// 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_HandleResize();
mListener->TP_DisplayStatusMessage(wxT("")); //STM: Clear message if all tracks are removed
}
}
// 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)
{
if (mUIHandle)
mUIHandle->OnProjectChange(GetProject());
// Tracks may have been deleted, so check to see if the focused track was on of them.
if (!mTracks->Contains(GetFocusedTrack())) {
SetFocusedTrack(NULL);
}
if (mCapturedTrack &&
!mTracks->Contains(mCapturedTrack)) {
SetCapturedTrack(nullptr);
if (HasCapture())
ReleaseMouse();
}
if (mFreqSelTrack &&
!mTracks->Contains(mFreqSelTrack)) {
mFreqSelTrack = nullptr;
if (HasCapture())
ReleaseMouse();
}
if (mPopupMenuTarget &&
!mTracks->Contains(mPopupMenuTarget)) {
mPopupMenuTarget = nullptr;
if (HasCapture())
ReleaseMouse();
}
GetSelectionState().TrackListUpdated( *mTracks );
if (e.GetClientData()) {
OnTrackListResized(e);
return;
}
e.Skip();
}
void TrackPanel::OnContextMenu(wxContextMenuEvent & WXUNUSED(event))
{
OnTrackMenu();
}
struct TrackInfo::TCPLine {
using DrawFunction = void (*)(
wxDC *dc,
const wxRect &rect,
const Track *maybeNULL,
int pressed, // a value from MouseCaptureEnum; TODO: make it bool
bool captured
);
unsigned items; // a bitwise OR of values of the enum above
int height;
int extraSpace;
DrawFunction drawFunction;
};
namespace {
#define RANGE(array) (array), (array) + sizeof(array)/sizeof(*(array))
using TCPLines = std::vector< TrackInfo::TCPLine >;
enum : unsigned {
// The sequence is not significant, just keep bits distinct
kItemBarButtons = 1 << 0,
kItemStatusInfo1 = 1 << 1,
kItemMute = 1 << 2,
kItemSolo = 1 << 3,
kItemGain = 1 << 4,
kItemPan = 1 << 5,
kItemVelocity = 1 << 6,
kItemMidiControlsRect = 1 << 7,
kItemMinimize = 1 << 8,
kItemSyncLock = 1 << 9,
kItemStatusInfo2 = 1 << 10,
kHighestBottomItem = kItemMinimize,
};
#ifdef EXPERIMENTAL_DA
#define TITLE_ITEMS \
{ kItemBarButtons, kTrackInfoBtnSize, 4, \
&TrackInfo::CloseTitleDrawFunction },
// DA: Has Mute and Solo on separate lines.
#define MUTE_SOLO_ITEMS(extra) \
{ kItemMute, kTrackInfoBtnSize + 1, 1, \
&TrackInfo::WideMuteDrawFunction }, \
{ kItemSolo, kTrackInfoBtnSize + 1, extra, \
&TrackInfo::WideSoloDrawFunction },
// DA: Does not have status information for a track.
#define STATUS_ITEMS
#else
#define TITLE_ITEMS \
{ kItemBarButtons, kTrackInfoBtnSize, 0, \
&TrackInfo::CloseTitleDrawFunction },
#define MUTE_SOLO_ITEMS(extra) \
{ kItemMute | kItemSolo, kTrackInfoBtnSize + 1, extra, \
&TrackInfo::MuteAndSoloDrawFunction },
#define STATUS_ITEMS \
{ kItemStatusInfo1, 12, 0, \
&TrackInfo::Status1DrawFunction }, \
{ kItemStatusInfo2, 12, 0, \
&TrackInfo::Status2DrawFunction },
#endif
#define COMMON_ITEMS \
TITLE_ITEMS
const TrackInfo::TCPLine defaultCommonTrackTCPLines[] = {
COMMON_ITEMS
};
TCPLines commonTrackTCPLines{ RANGE(defaultCommonTrackTCPLines) };
const TrackInfo::TCPLine defaultWaveTrackTCPLines[] = {
COMMON_ITEMS
MUTE_SOLO_ITEMS(2)
{ kItemGain, kTrackInfoSliderHeight, kTrackInfoSliderExtra,
&TrackInfo::GainSliderDrawFunction },
{ kItemPan, kTrackInfoSliderHeight, kTrackInfoSliderExtra,
&TrackInfo::PanSliderDrawFunction },
STATUS_ITEMS
};
TCPLines waveTrackTCPLines{ RANGE(defaultWaveTrackTCPLines) };
const TrackInfo::TCPLine defaultNoteTrackTCPLines[] = {
COMMON_ITEMS
#ifdef EXPERIMENTAL_MIDI_OUT
MUTE_SOLO_ITEMS(0)
{ kItemMidiControlsRect, kMidiCellHeight * 4, 0,
&TrackInfo::MidiControlsDrawFunction },
{ kItemVelocity, kTrackInfoSliderHeight, kTrackInfoSliderExtra,
&TrackInfo::VelocitySliderDrawFunction },
#endif
};
TCPLines noteTrackTCPLines{ RANGE(defaultNoteTrackTCPLines) };
int totalTCPLines( const TCPLines &lines, bool omitLastExtra )
{
int total = 0;
int lastExtra = 0;
for ( const auto line : lines ) {
lastExtra = line.extraSpace;
total += line.height + lastExtra;
}
if (omitLastExtra)
total -= lastExtra;
return total;
}
const TCPLines &getTCPLines( const Track &track )
{
#ifdef USE_MIDI
if ( track.GetKind() == Track::Note )
return noteTrackTCPLines;
#endif
if ( track.GetKind() == Track::Wave )
return waveTrackTCPLines;
return commonTrackTCPLines;
}
// return y value and height
std::pair< int, int > CalcItemY( const TCPLines &lines, unsigned iItem )
{
int y = 0;
auto pLines = lines.begin();
while ( pLines != lines.end() &&
0 == (pLines->items & iItem) ) {
y += pLines->height + pLines->extraSpace;
++pLines;
}
int height = 0;
if ( pLines != lines.end() )
height = pLines->height;
return { y, height };
}
// Items for the bottom of the panel, listed bottom-upwards
// As also with the top items, the extra space is below the item
const TrackInfo::TCPLine defaultCommonTrackTCPBottomLines[] = {
// The '0' avoids impinging on bottom line of TCP
// Use -1 if you do want to do so.
{ kItemSyncLock | kItemMinimize, kTrackInfoBtnSize, 0,
&TrackInfo::MinimizeSyncLockDrawFunction },
};
TCPLines commonTrackTCPBottomLines{ RANGE(defaultCommonTrackTCPBottomLines) };
// return y value and height
std::pair< int, int > CalcBottomItemY
( const TCPLines &lines, unsigned iItem, int height )
{
int y = height;
auto pLines = lines.begin();
while ( pLines != lines.end() &&
0 == (pLines->items & iItem) ) {
y -= pLines->height + pLines->extraSpace;
++pLines;
}
if (pLines != lines.end())
y -= (pLines->height + pLines->extraSpace );
return { y, pLines->height };
}
}
bool TrackInfo::HideTopItem( const wxRect &rect, const wxRect &subRect,
int allowance ) {
auto limit = CalcBottomItemY
( commonTrackTCPBottomLines, kHighestBottomItem, rect.height).first;
// Return true if the rectangle is even touching the limit
// without an overlap. That was the behavior as of 2.1.3.
return subRect.y + subRect.height - allowance >= rect.y + limit;
}
/// 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();
const auto foundCell = FindCell(event.m_x, event.m_y);
auto &t = foundCell.pTrack;
auto &rect = foundCell.rect;
{
#ifdef USE_MIDI
// DM: If it's a NoteTrack, it has special controls
if (t->GetKind() == Track::Note)
{
#ifdef EXPERIMENTAL_MIDI_OUT
wxRect midiRect;
mTrackInfo.GetMidiControlsRect(rect, midiRect);
bool isright = event.Button(wxMOUSE_BTN_RIGHT);
if ( !TrackInfo::HideTopItem( rect, midiRect ) &&
(isleft || isright) && midiRect.Contains(event.m_x, event.m_y) &&
static_cast<NoteTrack *>(t)->LabelClick(midiRect, event.m_x, event.m_y, isright)) {
MakeParentModifyState(false);
Refresh(false);
return;
}
#endif
}
#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) {
mRearrangeCount = 0;
SetCapturedTrack(t, IsRearranging);
TrackPanel::CalculateRearrangingThresholds(event);
}
GetProject()->HandleListSelection(t, event.ShiftDown(), event.ControlDown(), !unsafe);
}
/// 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()) {
if (mRearrangeCount != 0) {
wxString dir;
/* i18n-hint: a direction as in up or down.*/
dir = mRearrangeCount < 0 ? _("up") : _("down");
/* i18n-hint: will substitute name of track for first %s, "up" or "down" for the other.*/
MakeParentPushState(wxString::Format(_("Moved '%s' %s"),
mCapturedTrack->GetName().c_str(),
dir.c_str()),
_("Move Track"));
}
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.
if (event.m_y < mMoveUpThreshold || event.m_y < 0) {
mTracks->MoveUp(mCapturedTrack);
--mRearrangeCount;
if (pMixerBoard)
if(auto pPlayable = dynamic_cast< const PlayableTrack* >( mCapturedTrack ))
pMixerBoard->MoveTrackCluster(pPlayable, true /* up */);
}
else if (event.m_y > mMoveDownThreshold || event.m_y > GetRect().GetHeight()) {
mTracks->MoveDown(mCapturedTrack);
++mRearrangeCount;
if (pMixerBoard)
if(auto pPlayable = dynamic_cast< const PlayableTrack* >( mCapturedTrack ))
pMixerBoard->MoveTrackCluster(pPlayable, false /* down */);
}
else
{
return;
}
// 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;
}
/// 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 )
{
// Get here only if the click was near the bottom of the cell rectangle.
// DM: Figure out what track is about to be resized
const auto foundCell = FindCell(event.m_x, event.m_y);
auto track = foundCell.pTrack;
if (foundCell.type == CellType::Label && track && track->GetLinked())
// Click was at the bottom of a stereo track.
track = track->GetLink();
if (!track) {
return;
}
mMouseClickY = event.m_y;
#ifdef EXPERIMENTAL_OUTPUT_DISPLAY
// To do: escape key
if(MONO_WAVE_PAN(track)){
//STM: Determine whether we should rescale one or two tracks
if (track->GetVirtualStereo()) {
// mCapturedTrack is the lower track
mInitialTrackHeight = track->GetHeight(true);
mInitialActualHeight = mInitialUpperActualHeight = track->GetActualHeight();
mInitialMinimized = track->GetMinimized();
mInitialUpperTrackHeight = track->GetHeight();
SetCapturedTrack(track, IsResizingBelowLinkedTracks);
}
else {
// mCapturedTrack is the upper track
mInitialTrackHeight = track->GetHeight(true);
mInitialActualHeight = mInitialUpperActualHeight = track->GetActualHeight();
mInitialMinimized = track->GetMinimized();
mInitialUpperTrackHeight = track->GetHeight();
SetCapturedTrack(track, IsResizingBetweenLinkedTracks);
}
}
else
#endif
{
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);
}
}
}
/// 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
}
// Common pieces of code for MONO_WAVE_PAN and otherwise.
auto doResizeBelow = [&] (Track *prev, bool vStereo) {
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
#ifdef EXPERIMENTAL_OUTPUT_DISPLAY
, vStereo
#endif
);
prev->SetHeight(newUpperTrackHeight);
};
auto doResizeBetween = [&] (Track *next, bool vStereo) {
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();
}
#ifdef EXPERIMENTAL_OUTPUT_DISPLAY
if (vStereo) {
float temp = 1.0f;
if(newUpperTrackHeight != 0.0f)
temp = (float)newUpperTrackHeight/(float)(newUpperTrackHeight + newTrackHeight);
mCapturedTrack->SetVirtualTrackPercentage(temp);
}
#endif
mCapturedTrack->SetHeight(newUpperTrackHeight);
next->SetHeight(newTrackHeight
#ifdef EXPERIMENTAL_OUTPUT_DISPLAY
, vStereo
#endif
);
};
auto doResize = [&] {
int newTrackHeight = mInitialTrackHeight + delta;
if (newTrackHeight < mCapturedTrack->GetMinimizedHeight())
newTrackHeight = mCapturedTrack->GetMinimizedHeight();
mCapturedTrack->SetHeight(newTrackHeight);
};
//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
if(MONO_WAVE_PAN(mCapturedTrack)) {
switch( mMouseCapture )
{
case IsResizingBelowLinkedTracks:
{
doResizeBelow( mCapturedTrack, true );
break;
}
case IsResizingBetweenLinkedTracks:
{
doResizeBetween( mCapturedTrack, true );
break;
}
case IsResizing:
{
// Should imply !MONO_WAVE_PAN(mCapturedTrack),
// so impossible, but anyway:
doResize();
break;
}
default:
// don't refresh in this case.
return;
}
}
else
#endif
{
switch( mMouseCapture )
{
case IsResizingBelowLinkedTracks:
{
Track *prev = mTracks->GetPrev(mCapturedTrack);
doResizeBelow(prev, false);
break;
}
case IsResizingBetweenLinkedTracks:
{
Track *next = mTracks->GetNext(mCapturedTrack);
doResizeBetween(next, false);
break;
}
case IsResizing:
{
doResize();
break;
}
default:
// don't refresh in this case.
return;
}
}
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)
{
double steps {};
#if defined(__WXMAC__) && defined(EVT_MAGNIFY)
// PRL:
// Pinch and spread implemented in wxWidgets 3.1.0, or cherry-picked from
// the future in custom build of 3.0.2
if (event.Magnify()) {
event.SetControlDown(true);
steps = 2 * event.GetMagnification();
}
else
#endif
{
steps = event.m_wheelRotation /
(event.m_wheelDelta > 0 ? (double)event.m_wheelDelta : 120.0);
}
if(event.GetWheelAxis() == wxMOUSE_WHEEL_HORIZONTAL) {
// Two-fingered horizontal swipe on mac is treated like shift-mousewheel
event.SetShiftDown(true);
// This makes the wave move in the same direction as the fingers, and the scrollbar
// thumb moves oppositely
steps *= -1;
}
if(!event.HasAnyModifiers()) {
// We will later un-skip if we do anything, but if we don't,
// propagate the event up for the sake of the scrubber
event.Skip();
event.ResumePropagation(wxEVENT_PROPAGATE_MAX);
}
// Delegate wheel handling to the cell under the mouse, if it knows how.
{
const auto foundCell = FindCell( event.m_x, event.m_y );
auto &rect = foundCell.rect;
auto pCell = foundCell.pCell;
auto pTrack = foundCell.pTrack;
if (pCell) {
const auto size = GetSize();
unsigned result = pCell->HandleWheelRotation(
TrackPanelMouseEvent{ event, rect, size, pCell, steps },
GetProject() );
ProcessUIHandleResult(this, mRuler, pTrack, pTrack, result);
if (!(result & RefreshCode::Cancelled))
return;
}
}
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;
// Special case of pointer in the vertical ruler
if (event.ShiftDown() || event.CmdDown()) {
const auto foundCell = FindCell(event.m_x, event.m_y);
auto &pTrack = foundCell.pTrack;
auto &rect = foundCell.rect;
if (pTrack && foundCell.type == CellType::VRuler) {
HandleWheelRotationInVRuler(event, steps, pTrack, rect);
// Always stop propagation even if the ruler didn't change. The ruler
// is a narrow enough target.
event.Skip(false);
return;
}
}
if (event.ShiftDown()
// Don't pan during smooth scrolling. That would conflict with keeping
// the play indicator centered.
&& !GetProject()->GetScrubber().IsScrollScrubbing())
{
// 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;
if (GetProject()->GetScrubber().IsScrollScrubbing()) {
// Expand or contract about the center, ignoring mouse position
center_h = mViewInfo->h + (GetScreenEndTime() - mViewInfo->h) / 2.0;
xx = mViewInfo->TimeToPosition(center_h, trackLeftEdge);
}
else {
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 (GetProject()->GetScrubber().IsScrubbing()) {
GetProject()->GetScrubber().HandleScrollWheel(steps);
event.Skip(false);
}
else
#endif
{
// MM: Scroll up/down when used without modifier keys
double lines = steps * 4 + mVertScrollRemainder;
mVertScrollRemainder = lines - floor(lines);
lines = floor(lines);
const bool didSomething = mListener->TP_ScrollUpDown((int)-lines);
event.Skip(!didSomething);
}
}
}
void TrackPanel::HandleWheelRotationInVRuler
(wxMouseEvent &event, double steps, Track *pTrack, const wxRect &rect)
{
if (pTrack->GetKind() == Track::Wave) {
WaveTrack *const wt = static_cast<WaveTrack*>(pTrack);
// Assume linked track is wave or null
const auto partner = static_cast<WaveTrack*>(wt->GetLink());
const bool isDB =
wt->GetDisplay() == WaveTrack::Waveform &&
wt->GetWaveformSettings().scaleType == WaveformSettings::stLogarithmic;
// Special cases for Waveform dB only.
// Set the bottom of the dB scale but only if it's visible
if (isDB && event.ShiftDown() && event.CmdDown()) {
float min, max;
wt->GetDisplayBounds(&min, &max);
if (min < 0.0 && max > 0.0) {
WaveformSettings &settings = wt->GetIndependentWaveformSettings();
float olddBRange = settings.dBRange;
if (steps < 0)
// Zoom out
settings.NextLowerDBRange();
else
settings.NextHigherDBRange();
float newdBRange = settings.dBRange;
if (partner) {
WaveformSettings &settings = partner->GetIndependentWaveformSettings();
if (steps < 0)
// Zoom out
settings.NextLowerDBRange();
else
settings.NextHigherDBRange();
}
// Is y coordinate within the rectangle half-height centered about
// the zero level?
const auto zeroLevel = wt->ZeroLevelYCoordinate(rect);
const bool fixedMagnification =
(4 * std::abs(event.GetY() - zeroLevel) < rect.GetHeight());
if (fixedMagnification) {
// Vary the db limit without changing
// magnification; that is, peaks and troughs move up and down
// rigidly, as parts of the wave near zero are exposed or hidden.
const float extreme = (LINEAR_TO_DB(2) + newdBRange) / newdBRange;
max = std::min(extreme, max * olddBRange / newdBRange);
min = std::max(-extreme, min * olddBRange / newdBRange);
wt->SetLastdBRange();
wt->SetDisplayBounds(min, max);
if (partner) {
partner->SetLastdBRange();
partner->SetDisplayBounds(min, max);
}
}
}
}
else if (event.CmdDown() && !event.ShiftDown()) {
HandleWaveTrackVZoom(
GetTracks(), rect, event.m_y, event.m_y,
wt, false, (steps < 0),
true);
}
else if (!event.CmdDown() && event.ShiftDown()) {
// Scroll some fixed number of pixels, independent of zoom level or track height:
static const float movement = 10.0f;
const int height = wt->GetHeight() - (kTopMargin + kBottomMargin);
const bool spectral = (wt->GetDisplay() == WaveTrack::Spectrum);
if (spectral) {
const float delta = steps * movement / height;
SpectrogramSettings &settings = wt->GetIndependentSpectrogramSettings();
const bool isLinear = settings.scaleType == SpectrogramSettings::stLinear;
float bottom, top;
wt->GetSpectrumBounds(&bottom, &top);
const double rate = wt->GetRate();
const float bound = rate / 2;
const NumberScale numberScale( settings.GetScale( bottom, top ) );
float newTop =
std::min(bound, numberScale.PositionToValue(1.0f + delta));
const float newBottom =
std::max((isLinear ? 0.0f : 1.0f),
numberScale.PositionToValue(numberScale.ValueToPosition(newTop) - 1.0f));
newTop =
std::min(bound,
numberScale.PositionToValue(numberScale.ValueToPosition(newBottom) + 1.0f));
wt->SetSpectrumBounds(newBottom, newTop);
if (partner)
partner->SetSpectrumBounds(newBottom, newTop);
}
else {
float topLimit = 2.0;
if (isDB) {
const float dBRange = wt->GetWaveformSettings().dBRange;
topLimit = (LINEAR_TO_DB(topLimit) + dBRange) / dBRange;
}
const float bottomLimit = -topLimit;
float top, bottom;
wt->GetDisplayBounds(&bottom, &top);
const float range = top - bottom;
const float delta = range * steps * movement / height;
float newTop = std::min(topLimit, top + delta);
const float newBottom = std::max(bottomLimit, newTop - range);
newTop = std::min(topLimit, newBottom + range);
wt->SetDisplayBounds(newBottom, newTop);
if (partner)
partner->SetDisplayBounds(newBottom, newTop);
}
}
else
return;
UpdateVRuler(pTrack);
Refresh(false);
MakeParentModifyState(true);
}
else {
// To do: time track? Note track?
}
return;
}
/// Filter captured keys typed into LabelTracks.
void TrackPanel::OnCaptureKey(wxCommandEvent & event)
{
wxKeyEvent *kevent = static_cast<wxKeyEvent *>(event.GetEventObject());
if ( WXK_ESCAPE != kevent->GetKeyCode() )
HandleInterruptedDrag();
Track * const t = GetFocusedTrack();
if (t && t->GetKind() == Track::Label)
event.Skip(!((LabelTrack *)t)->CaptureKey(*kevent));
else
if (t) {
const unsigned refreshResult =
((TrackPanelCell*)t)->CaptureKey(*kevent, *mViewInfo, this);
ProcessUIHandleResult(this, mRuler, t, t, refreshResult);
}
else
event.Skip();
}
void TrackPanel::OnKeyDown(wxKeyEvent & event)
{
switch (event.GetKeyCode())
{
case WXK_ESCAPE:
if(HandleEscapeKey(true))
// Don't skip the event, eat it so that
// AudacityApp does not also stop any playback.
return;
else
break;
case WXK_ALT:
HandleAltKey(true);
break;
case WXK_SHIFT:
HandleShiftKey(true);
break;
case WXK_CONTROL:
HandleControlKey(true);
break;
// Allow PageUp and PageDown keys to
//scroll the Track Panel left and right
case WXK_PAGEUP:
HandlePageUpKey();
return;
case WXK_PAGEDOWN:
HandlePageDownKey();
return;
}
Track *const t = GetFocusedTrack();
if (t && t->GetKind() == Track::Label) {
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"),
UndoPush::CONSOLIDATE);
// Make sure caret is in view
int x;
if (lt->CalcCursorX(&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);
}
else
if (t) {
const unsigned refreshResult =
((TrackPanelCell*)t)->KeyDown(event, *mViewInfo, this);
ProcessUIHandleResult(this, mRuler, t, t, refreshResult);
}
else
event.Skip();
}
void TrackPanel::OnChar(wxKeyEvent & event)
{
switch (event.GetKeyCode())
{
case WXK_ESCAPE:
case WXK_ALT:
case WXK_SHIFT:
case WXK_CONTROL:
case WXK_PAGEUP:
case WXK_PAGEDOWN:
return;
}
Track *const t = GetFocusedTrack();
if (t && t->GetKind() == Track::Label) {
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"),
UndoPush::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);
}
else
if (t) {
const unsigned refreshResult =
((TrackPanelCell*)t)->Char(event, *mViewInfo, this);
ProcessUIHandleResult(this, mRuler, t, t, refreshResult);
}
else
event.Skip();
}
void TrackPanel::OnKeyUp(wxKeyEvent & event)
{
bool didSomething = false;
switch (event.GetKeyCode())
{
case WXK_ESCAPE:
didSomething = HandleEscapeKey(false);
break;
case WXK_ALT:
HandleAltKey(false);
break;
case WXK_SHIFT:
HandleShiftKey(false);
break;
case WXK_CONTROL:
HandleControlKey(false);
break;
}
if (didSomething)
return;
Track * const t = GetFocusedTrack();
if (t) {
const unsigned refreshResult =
((TrackPanelCell*)t)->KeyUp(event, *mViewInfo, this);
ProcessUIHandleResult(this, mRuler, t, t, refreshResult);
return;
}
event.Skip();
}
/// 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)
try
{
#if defined(__WXMAC__) && defined(EVT_MAGNIFY)
// PRL:
// Pinch and spread implemented in wxWidgets 3.1.0, or cherry-picked from
// the future in custom build of 3.0.2
if (event.Magnify()) {
HandleWheelRotation(event);
}
#endif
// If a mouse event originates from a keyboard context menu event then
// event.GetPosition() == wxDefaultPosition. wxContextMenu events are handled in
// TrackPanel::OnContextMenu(), and therefore associated mouse events are ignored here.
// Not ignoring them was causing bug 613: the mouse events were interpreted as clicking
// outside the tracks.
if (event.GetPosition() == wxDefaultPosition && (event.RightDown() || event.RightUp())) {
event.Skip();
return;
}
if (event.m_wheelRotation != 0)
HandleWheelRotation(event);
if (event.LeftDown() || event.LeftIsDown() || event.Moving()) {
// Skip, even if we do something, so that the left click or drag
// may have an additional effect in the scrubber.
event.Skip();
event.ResumePropagation(wxEVENT_PROPAGATE_MAX);
}
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())
{
auto buttons =
// Bug 1325: button state in Leaving events is unreliable on Mac.
// Poll the global state instead.
// event.ButtonIsDown(wxMOUSE_BTN_ANY);
::wxGetMouseState().ButtonIsDown(wxMOUSE_BTN_ANY);
if(!buttons) {
HandleEscapeKey( true );
#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
}
}
if (mUIHandle) {
const auto foundCell = FindCell( event.m_x, event.m_y );
auto &rect = foundCell.rect;
auto &pCell = foundCell.pCell;
auto &pTrack = foundCell.pTrack;
const auto size = GetSize();
if (event.Dragging()) {
// UIHANDLE DRAG
const UIHandle::Result refreshResult = mUIHandle->Drag(
TrackPanelMouseEvent{ event, rect, size, pCell }, GetProject() );
ProcessUIHandleResult(this, mRuler, mpClickedTrack, pTrack, refreshResult);
if (refreshResult & RefreshCode::Cancelled) {
// Drag decided to abort itself
mUIHandle = NULL;
mpClickedTrack = NULL;
if (HasCapture())
ReleaseMouse();
// Should this be done? As for cancelling?
// HandleCursor(event);
}
else {
// UIHANDLE PREVIEW
// Update status message and cursor during drag
HitTestPreview preview = mUIHandle->Preview(
TrackPanelMouseEvent{ event, rect, size, pCell }, GetProject() );
mListener->TP_DisplayStatusMessage(preview.message);
if (preview.cursor)
SetCursor(*preview.cursor);
}
}
else if (event.ButtonUp()) {
// UIHANDLE RELEASE
UIHandle::Result refreshResult = mUIHandle->Release(
TrackPanelMouseEvent{ event, rect, size, pCell }, GetProject(),
this );
ProcessUIHandleResult(this, mRuler, mpClickedTrack, pTrack, refreshResult);
mUIHandle = NULL;
mpClickedTrack = NULL;
// ReleaseMouse() already done above
// Should this be done? As for cancelling?
// HandleCursor(event);
}
}
else switch( mMouseCapture ) {
case IsVZooming:
HandleVZoom(event);
break;
case IsResizing:
case IsResizingBetweenLinkedTracks:
case IsResizingBelowLinkedTracks:
HandleResize(event);
HandleCursor(event);
break;
case IsRearranging:
HandleRearrange(event);
break;
case IsAdjustingLabel:
// Reach this case only when the captured track was label
HandleGlyphDragRelease(static_cast<LabelTrack *>(mCapturedTrack), event);
break;
case IsSelectingLabelText:
// Reach this case only when the captured track was label
HandleTextDragRelease(static_cast<LabelTrack *>(mCapturedTrack), 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 rect;
const auto foundCell = FindCell(event.m_x, event.m_y);
auto t = foundCell.pTrack;
if (t
&& foundCell.type == CellType::Track
)
EnsureVisible(t);
}
}
catch( ... )
{
// Abort any dragging, as if by hitting Esc
if ( HandleEscapeKey( true ) )
;
else {
// Ensure these steps, if escape handling did nothing
SetCapturedTrack(NULL, IsUncaptured);
if (HasCapture())
ReleaseMouse();
wxMouseEvent dummy;
HandleCursor(dummy);
Refresh(false);
}
throw;
}
/// Event has happened on a track and it has been determined to be a label track.
bool TrackPanel::HandleLabelTrackClick(LabelTrack * lTrack, const wxRect &rect, wxMouseEvent & event)
{
if (!event.ButtonDown())
return false;
if(event.LeftDown())
{
/// \todo This method is one of a large number of methods in
/// TrackPanel which suitably modified belong in other classes.
TrackListIterator iter(GetTracks());
Track *n = iter.First();
while (n) {
if (n->GetKind() == Track::Label && lTrack != n) {
((LabelTrack *)n)->ResetFlags();
((LabelTrack *)n)->Unselect();
}
n = iter.Next();
}
}
mCapturedRect = rect;
lTrack->HandleClick(event, mCapturedRect, *mViewInfo, &mViewInfo->selectedRegion);
if (lTrack->IsAdjustingLabel())
{
SetCapturedTrack(lTrack, IsAdjustingLabel);
//If we are adjusting a label on a labeltrack, do not do anything
//that follows. Instead, redraw the track.
RefreshTrack(lTrack);
return true;
}
if( event.LeftDown() ){
bool bShift = event.ShiftDown();
bool bCtrlDown = event.ControlDown();
bool unsafe = IsUnsafe();
if( /*bShift ||*/ bCtrlDown ){
GetProject()->HandleListSelection(lTrack, bShift, bCtrlDown, !unsafe);
return true;
}
}
// IF the user clicked a label, THEN select all other tracks by Label
if (lTrack->IsSelected()) {
SelectTracksByLabel(lTrack);
// Do this after, for the effect on mLastPickedTrack:
GetSelectionState().SelectTrack
( *mTracks, *lTrack, true, true, GetMixerBoard() );
DisplaySelection();
// Not starting a drag
SetCapturedTrack(NULL, IsUncaptured);
if(mCapturedTrack == NULL)
SetCapturedTrack(lTrack, IsSelectingLabelText);
RefreshTrack(lTrack);
// PRL: bug1659 -- make selection change undo correctly
if (!IsUnsafe())
MakeParentModifyState(false);
return true;
}
// handle shift+ctrl down
/*if (event.ShiftDown()) { // && event.ControlDown()) {
lTrack->SetHighlightedByKey(true);
Refresh(false);
return;
}*/
// return false, there is more to do...
return false;
}
/// Event has happened on a track and it has been determined to be a label track.
void TrackPanel::HandleGlyphDragRelease(LabelTrack * lTrack, wxMouseEvent & event)
{
if (!lTrack)
return;
/// \todo This method is one of a large number of methods in
/// TrackPanel which suitably modified belong in other classes.
if (event.Dragging()) {
;
}
else if (event.LeftUp())
SetCapturedTrack(NULL);
if (lTrack->HandleGlyphDragRelease(event, mCapturedRect,
*mViewInfo, &mViewInfo->selectedRegion)) {
MakeParentPushState(_("Modified Label"),
_("Label Edit"),
UndoPush::CONSOLIDATE);
}
// Update cursor on the screen if it is a point.
DrawOverlays(false);
mRuler->DrawOverlays(false);
//If we are adjusting a label on a labeltrack, do not do anything
//that follows. Instead, redraw the track.
RefreshTrack(lTrack);
return;
}
/// Event has happened on a track and it has been determined to be a label track.
void TrackPanel::HandleTextDragRelease(LabelTrack * lTrack, wxMouseEvent & event)
{
if (!lTrack)
return;
lTrack->HandleTextDragRelease(event);
/// \todo This method is one of a large number of methods in
/// TrackPanel which suitably modified belong in other classes.
if (event.Dragging()) {
;
}
else if (event.ButtonUp())
SetCapturedTrack(NULL);
// 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;
}
}
}
// handle mouse left button up
if (event.LeftUp()) {
mLabelTrackStartXPos = -1;
}
}
// AS: I don't really understand why this code is sectioned off
// from the other OnMouseEvent code.
void TrackPanel::HandleTrackSpecificMouseEvent(wxMouseEvent & event)
{
const auto foundCell = FindCell( event.m_x, event.m_y );
auto &pTrack = foundCell.pTrack;
auto &pCell = foundCell.pCell;
auto &rect = foundCell.rect;
//call HandleResize if I'm over the border area
// (Add margin back to bottom of the rectangle)
if (event.LeftDown() &&
pTrack &&
(within(event.m_y,
(rect.GetBottom() + (kBottomMargin + kTopMargin) / 2),
TRACK_RESIZE_REGION))) {
HandleResize(event);
HandleCursor(event);
return;
}
// AS: If the user clicked outside all tracks, make nothing
// selected.
if ((event.ButtonDown() || event.ButtonDClick()) && !pTrack) {
GetSelectionState().SelectNone( *mTracks, GetMixerBoard() );
Refresh(false);
return;
}
//Determine if user clicked on the track's left-hand label or ruler
if ( !( foundCell.type == CellType::Track ||
foundCell.type == CellType::Background ) ) {
const auto size = GetSize();
if (!mUIHandle &&
pCell &&
(event.ButtonDown() || event.ButtonDClick()))
mUIHandle = pCell->HitTest(
TrackPanelMouseEvent{ event, rect, size }, GetProject()).handle;
if (mUIHandle) {
// UIHANDLE CLICK
UIHandle::Result refreshResult = mUIHandle->Click(
TrackPanelMouseEvent{ event, rect, size, pCell }, GetProject() );
if (refreshResult & RefreshCode::Cancelled)
mUIHandle = NULL;
else
mpClickedTrack = pTrack;
ProcessUIHandleResult(this, mRuler, pTrack, pTrack, refreshResult);
}
else {
if (foundCell.type == CellType::VRuler) {
if (!event.Dragging()) // JKC: Only want the mouse down event.
HandleVZoom(event);
}
else if (foundCell.type == CellType::Label)
HandleLabelClick(event);
}
HandleCursor(event);
return;
}
// To do: remove the following special things
// so that we can coalesce the code for track and non-track clicks
//Determine if user clicked on a label track.
//If so, use MouseDown handler for the label track.
if (!mUIHandle &&
pTrack && foundCell.type == CellType::Track &&
(pTrack->GetKind() == Track::Label))
{
if (HandleLabelTrackClick((LabelTrack *)pTrack, rect, event))
return;
}
bool handled = false;
ToolsToolBar * pTtb = mListener->TP_GetToolsToolBar();
if( !handled && pTtb != NULL &&
( foundCell.type == CellType::Track ||
foundCell.type == CellType::Background ) )
{
const auto size = GetSize();
if (pCell &&
(event.ButtonDown() || event.ButtonDClick()) &&
( mUIHandle ||
NULL != (mUIHandle = pCell->HitTest(
TrackPanelMouseEvent{ event, rect, size }, GetProject()).handle))) {
// UIHANDLE CLICK
UIHandle::Result refreshResult = mUIHandle->Click(
TrackPanelMouseEvent{ event, rect, size, pCell }, GetProject() );
if (refreshResult & RefreshCode::Cancelled)
mUIHandle = NULL;
else
mpClickedTrack = pTrack;
ProcessUIHandleResult(this, mRuler, pTrack, pTrack, refreshResult);
}
else {
int toolToUse = DetermineToolToUse(pTtb, event);
switch (toolToUse) {
case selectTool:
HandleSelect(event);
break;
}
}
}
if ((event.Moving() || event.LeftUp()) &&
(mMouseCapture == IsUncaptured ))
// (mMouseCapture != IsSelecting )
{
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, const 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..
const auto foundCell = FindCell(event.m_x, event.m_y);
auto &pTrack = foundCell.pTrack;
auto &rect = foundCell.rect;
if( !pTrack|| foundCell.type != CellType::Track )
return currentTool;
int trackKind = pTrack->GetKind();
currentTool = selectTool; // the default.
if( trackKind == Track::Label ){
currentTool = selectTool;
} else if( trackKind != Track::Wave) {
currentTool = selectTool;
// So we are in a wave track? Not necessarily.
// FIXME: Possibly not in wave track. 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.
}
//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
auto TrackPanel::HitTestStretch
( const Track *track, const wxRect &rect, const wxMouseEvent & event,
StretchState *pState )
-> StretchEnum
{
// 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 stretchNone;
}
int center = rect.y + rect.height / 2;
int distance = abs(event.m_y - center);
const int yTolerance = 10;
wxInt64 leftSel = mViewInfo->TimeToPosition(mViewInfo->selectedRegion.t0(), rect.x);
wxInt64 rightSel = mViewInfo->TimeToPosition(mViewInfo->selectedRegion.t1(), rect.x);
// Something is wrong if right edge comes before left edge
wxASSERT(!(rightSel < leftSel));
if (leftSel <= event.m_x && event.m_x <= rightSel &&
distance < yTolerance)
return ChooseStretchMode
( event, rect, *mViewInfo,
static_cast< const NoteTrack * >( track ), pState );
return stretchNone;
}
#endif
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();
}
// subtract insets and shadows from the rectangle, but not border
// This matters because some separators do paint over the border
wxRect rect(kLeftInset,
-mViewInfo->vpos + trk->GetY() + kTopInset,
GetRect().GetWidth() - kLeftInset - kRightInset - kShadowThickness,
trk->GetHeight() - kTopInset - kShadowThickness);
if (link) {
rect.height += link->GetHeight();
}
#ifdef EXPERIMENTAL_OUTPUT_DISPLAY
else if(MONO_WAVE_PAN(trk)){
rect.height += trk->GetHeight(true);
}
#endif
if( refreshbacking )
{
mRefreshBacking = true;
}
Refresh( false, &rect );
}
/// 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)
{
wxRegion region = GetUpdateRegion();
const 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(GetTracks(), 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 &region,
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 rect = trackRect;
bool skipBorder = false;
Track *l = t->GetLink();
if (t->GetLinked()) {
rect.height += l->GetHeight();
}
else if (l && trackRect.y >= 0) {
skipBorder = true;
}
#ifdef EXPERIMENTAL_OUTPUT_DISPLAY
if(MONO_WAVE_PAN(t)){
rect.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 = rect;
if (l && !t->GetLinked() && trackRect.y < 0)
{
borderTrack = l;
borderRect = trackRect;
borderRect.y = l->GetY() - mViewInfo->vpos;
borderRect.height = l->GetHeight();
borderRect.height += t->GetHeight();
}
if (!skipBorder) {
if (mAx->IsFocused(t)) {
focusRect = borderRect;
}
DrawOutside(borderTrack, dc, borderRect);
}
// 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 rect = trackRect;
rect.x += GetVRulerOffset();
rect.y += kTopMargin;
rect.width = GetVRulerWidth();
rect.height -= (kTopMargin + kBottomMargin);
mTrackArtist->DrawVRuler(t, dc, rect);
}
#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 rect = trackRect;
rect.x += GetVRulerOffset();
rect.y += kTopMargin;
rect.width = GetVRulerWidth();
rect.height -= (kTopMargin + kBottomMargin);
mTrackArtist->DrawVRuler(t, dc, rect);
}
}
#endif
}
if (mUIHandle)
mUIHandle->DrawExtras(UIHandle::Cells, dc, region, clip);
if (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);
}
if (mUIHandle)
mUIHandle->DrawExtras(UIHandle::Panel, dc, region, clip);
// Draw snap guidelines if we have any
if ( mSnapManager )
mSnapManager->Draw( dc, GetSnapLeft(), GetSnapRight() );
}
/// 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 rect;
dc->SetBrush(*wxTRANSPARENT_BRUSH);
dc->SetPen(*wxBLACK_DASHED_PEN);
if (mMouseCapture==IsVZooming) {
rect.y = std::min(mZoomStart, mZoomEnd);
rect.height = 1 + abs(mZoomEnd - mZoomStart);
rect.x = GetVRulerOffset();
rect.SetRight(GetSize().x - kRightMargin); // extends into border rect
}
else {
rect.x = std::min(mZoomStart, mZoomEnd);
rect.width = 1 + abs(mZoomEnd - mZoomStart);
rect.y = -1;
rect.height = clip.height + 2;
}
dc->DrawRectangle(rect);
}
// Make this #include go away!
#include "tracks/ui/TrackControls.h"
void TrackInfo::DrawItems
( wxDC *dc, const wxRect &rect, const Track &track,
int mouseCapture, bool captured )
{
const auto topLines = getTCPLines( track );
const auto bottomLines = commonTrackTCPBottomLines;
DrawItems
( dc, rect, &track, topLines, bottomLines, mouseCapture, captured );
}
void TrackInfo::DrawItems
( wxDC *dc, const wxRect &rect, const Track *pTrack,
const std::vector<TCPLine> &topLines, const std::vector<TCPLine> &bottomLines,
int mouseCapture, bool captured )
{
TrackInfo::SetTrackInfoFont(dc);
dc->SetTextForeground(theTheme.Colour(clrTrackPanelText));
{
int yy = 0;
for ( const auto &line : topLines ) {
wxRect itemRect{
rect.x, rect.y + yy,
rect.width, line.height
};
if ( !TrackInfo::HideTopItem( rect, itemRect ) &&
line.drawFunction )
line.drawFunction( dc, itemRect, pTrack, mouseCapture, captured );
yy += line.height + line.extraSpace;
}
}
{
int yy = rect.height;
for ( const auto &line : bottomLines ) {
yy -= line.height + line.extraSpace;
if ( line.drawFunction ) {
wxRect itemRect{
rect.x, rect.y + yy,
rect.width, line.height
};
line.drawFunction( dc, itemRect, pTrack, mouseCapture, captured );
}
}
}
}
void TrackInfo::CloseTitleDrawFunction
( wxDC *dc, const wxRect &rect, const Track *pTrack, int pressed, bool captured )
{
bool selected = pTrack ? pTrack->GetSelected() : true;
{
bool down = captured && (pressed == TrackPanel::IsClosing);
wxRect bev = rect;
GetCloseBoxHorizontalBounds( rect, bev );
AColor::Bevel2(*dc, !down, bev, selected );
#ifdef EXPERIMENTAL_THEMING
wxPen pen( theTheme.Colour( clrTrackPanelText ));
dc->SetPen( pen );
#else
dc->SetPen(*wxBLACK_PEN);
#endif
bev.Inflate( -1, -1 );
// 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);
}
{
wxString titleStr =
pTrack ? pTrack->GetName() : _("Name");
bool down = captured && (pressed == TrackPanel::IsPopping);
wxRect bev = rect;
GetTitleBarHorizontalBounds( rect, bev );
//bev.Inflate(-1, -1);
AColor::Bevel2(*dc, !down, bev, selected);
// Draw title text
SetTrackInfoFont(dc);
int allowableWidth = rect.width - 42;
wxCoord textWidth, textHeight;
dc->GetTextExtent(titleStr, &textWidth, &textHeight);
while (textWidth > allowableWidth) {
titleStr = titleStr.Left(titleStr.Length() - 1);
dc->GetTextExtent(titleStr, &textWidth, &textHeight);
}
// Pop-up triangle
#ifdef EXPERIMENTAL_THEMING
wxColour c = theTheme.Colour( clrTrackPanelText );
#else
wxColour c = *wxBLACK;
#endif
// 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->SetTextForeground( c );
dc->SetTextBackground( wxTRANSPARENT );
dc->DrawText(titleStr, bev.x + 2, bev.y + (bev.height - textHeight) / 2);
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);
}
}
void TrackInfo::MinimizeSyncLockDrawFunction
( wxDC *dc, const wxRect &rect, const Track *pTrack, int pressed, bool captured )
{
bool selected = pTrack ? pTrack->GetSelected() : true;
bool syncLockSelected = pTrack ? pTrack->IsSyncLockSelected() : true;
bool minimized = pTrack ? pTrack->GetMinimized() : false;
{
bool down = captured && (pressed == TrackPanel::IsMinimizing);
wxRect bev = rect;
GetMinimizeHorizontalBounds(rect, bev);
// Clear background to get rid of previous arrow
//AColor::MediumTrackInfo(dc, t->GetSelected());
//dc->DrawRectangle(bev);
AColor::Bevel2(*dc, !down, bev, selected);
#ifdef EXPERIMENTAL_THEMING
wxColour c = theTheme.Colour(clrTrackPanelText);
dc->SetBrush(c);
dc->SetPen(c);
#else
AColor::Dark(dc, selected);
#endif
AColor::Arrow(*dc,
bev.x - 5 + bev.width / 2,
bev.y - 2 + bev.height / 2,
10,
minimized);
}
// Draw the sync-lock indicator if this track is in a sync-lock selected group.
if (syncLockSelected)
{
wxRect syncLockIconRect = rect;
GetSyncLockHorizontalBounds( rect, syncLockIconRect );
wxBitmap syncLockBitmap(theTheme.Image(bmpSyncLockIcon));
// Icon is 12x12 and syncLockIconRect is 16x16.
dc->DrawBitmap(syncLockBitmap,
syncLockIconRect.x + 3,
syncLockIconRect.y + 2,
true);
}
}
void TrackInfo::MidiControlsDrawFunction
( wxDC *dc, const wxRect &rect, const Track *pTrack, int, bool )
{
#ifdef EXPERIMENTAL_MIDI_OUT
wxRect midiRect = rect;
GetMidiControlsHorizontalBounds(rect, midiRect);
NoteTrack::DrawLabelControls
( static_cast<const NoteTrack *>(pTrack), *dc, midiRect );
#endif // EXPERIMENTAL_MIDI_OUT
}
template<typename TrackClass>
void TrackInfo::SliderDrawFunction
( LWSlider *(*Selector)
(const wxRect &sliderRect, const TrackClass *t, bool captured, wxWindow*),
wxDC *dc, const wxRect &rect, const Track *pTrack, bool captured )
{
wxRect sliderRect = rect;
TrackInfo::GetSliderHorizontalBounds( rect.GetTopLeft(), sliderRect );
auto wt = static_cast<const TrackClass*>( pTrack );
Selector( sliderRect, wt, captured, nullptr )->OnPaint(*dc);
}
void TrackInfo::PanSliderDrawFunction
( wxDC *dc, const wxRect &rect, const Track *pTrack, int, bool captured )
{
SliderDrawFunction<WaveTrack>
( &TrackInfo::PanSlider, dc, rect, pTrack, captured);
}
void TrackInfo::GainSliderDrawFunction
( wxDC *dc, const wxRect &rect, const Track *pTrack, int, bool captured )
{
SliderDrawFunction<WaveTrack>
( &TrackInfo::GainSlider, dc, rect, pTrack, captured);
}
#ifdef EXPERIMENTAL_MIDI_OUT
void TrackInfo::VelocitySliderDrawFunction
( wxDC *dc, const wxRect &rect, const Track *pTrack, int, bool captured )
{
SliderDrawFunction<NoteTrack>
( &TrackInfo::VelocitySlider, dc, rect, pTrack, captured);
}
#endif
void TrackInfo::MuteOrSoloDrawFunction
( wxDC *dc, const wxRect &bev, const Track *pTrack, int pressed, bool captured,
bool solo )
{
bool down = captured &&
(pressed == ( solo ? TrackPanel::IsSoloing : TrackPanel::IsMuting ));
//bev.Inflate(-1, -1);
bool selected = pTrack ? pTrack->GetSelected() : true;
auto pt = dynamic_cast<const PlayableTrack *>(pTrack);
bool value = pt ? (solo ? pt->GetSolo() : pt->GetMute()) : false;
#if 0
AColor::MediumTrackInfo( dc, t->GetSelected());
if( solo )
{
if( pt && pt->GetSolo() )
{
AColor::Solo(dc, pt->GetSolo(), t->GetSelected());
}
}
else
{
if( pt && pt->GetMute() )
{
AColor::Mute(dc, pt->GetMute(), t->GetSelected(), pt->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);
#endif
wxCoord textWidth, textHeight;
wxString str = (solo) ?
/* i18n-hint: This is on a button that will silence all the other tracks.*/
_("Solo") :
/* i18n-hint: This is on a button that will silence this track.*/
_("Mute");
AColor::Bevel2(
*dc,
value == down,
bev,
selected
);
SetTrackInfoFont(dc);
dc->GetTextExtent(str, &textWidth, &textHeight);
dc->DrawText(str, bev.x + (bev.width - textWidth) / 2, bev.y + (bev.height - textHeight) / 2);
}
void TrackInfo::WideMuteDrawFunction
( wxDC *dc, const wxRect &rect, const Track *pTrack, int pressed, bool captured )
{
wxRect bev = rect;
GetWideMuteSoloHorizontalBounds( rect, bev );
MuteOrSoloDrawFunction( dc, bev, pTrack, pressed, captured, false );
}
void TrackInfo::WideSoloDrawFunction
( wxDC *dc, const wxRect &rect, const Track *pTrack, int pressed, bool captured )
{
wxRect bev = rect;
GetWideMuteSoloHorizontalBounds( rect, bev );
MuteOrSoloDrawFunction( dc, bev, pTrack, pressed, captured, true );
}
void TrackInfo::MuteAndSoloDrawFunction
( wxDC *dc, const wxRect &rect, const Track *pTrack, int pressed, bool captured )
{
bool bHasSoloButton = TrackPanel::HasSoloButton();
wxRect bev = rect;
if ( bHasSoloButton )
GetNarrowMuteHorizontalBounds( rect, bev );
else
GetWideMuteSoloHorizontalBounds( rect, bev );
MuteOrSoloDrawFunction( dc, bev, pTrack, pressed, captured, false );
if( !bHasSoloButton )
return;
GetNarrowSoloHorizontalBounds( rect, bev );
MuteOrSoloDrawFunction( dc, bev, pTrack, pressed, captured, true );
}
void TrackInfo::StatusDrawFunction
( const wxString &string, wxDC *dc, const wxRect &rect )
{
static const int offset = 3;
dc->DrawText(string, rect.x + offset, rect.y);
}
void TrackInfo::Status1DrawFunction
( wxDC *dc, const wxRect &rect, const Track *pTrack, int, bool )
{
auto wt = static_cast<const WaveTrack*>(pTrack);
/// 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.
auto rate = wt ? wt->GetRate() : 44100.0;
wxString s = wxString::Format(wxT("%dHz"), (int) (rate + 0.5));
if (!wt || (wt->GetLinked()
#ifdef EXPERIMENTAL_OUTPUT_DISPLAY
&& wt->GetChannel() != Track::MonoChannel
#endif
))
s = _("Stereo, ") + s;
else {
if (wt->GetChannel() == Track::MonoChannel)
s = _("Mono, ") + s;
else if (wt->GetChannel() == Track::LeftChannel)
s = _("Left, ") + s;
else if (wt->GetChannel() == Track::RightChannel)
s = _("Right, ") + s;
}
StatusDrawFunction( s, dc, rect );
}
void TrackInfo::Status2DrawFunction
( wxDC *dc, const wxRect &rect, const Track *pTrack, int, bool )
{
auto wt = static_cast<const WaveTrack*>(pTrack);
auto format = wt ? wt->GetSampleFormat() : floatSample;
auto s = GetSampleFormatStr(format);
StatusDrawFunction( s, dc, rect );
}
void TrackPanel::DrawOutside(Track * t, wxDC * dc, const wxRect & rec)
{
bool bIsWave = (t->GetKind() == Track::Wave);
// Draw things that extend right of track control panel
{
// Start with whole track rect
wxRect rect = rec;
DrawOutsideOfTrack(t, dc, rect);
// Now exclude left, right, and top insets
rect.x += kLeftInset;
rect.y += kTopInset;
rect.width -= kLeftInset * 2;
rect.height -= kTopInset;
int labelw = GetLabelWidth();
int vrul = GetVRulerOffset();
mTrackInfo.DrawBackground(dc, rect, 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 = rect;
// tileFill.x = GetVRulerOffset();
// tileFill.width = GetVRulerWidth();
// TrackArtist::DrawSyncLockTiles(dc, tileFill);
//}
DrawBordersAroundTrack(t, dc, rect, labelw, vrul);
DrawShadow(t, dc, rect);
}
// Draw things within the track control panel
wxRect rect = rec;
rect.x += kLeftMargin;
rect.width = kTrackInfoWidth - kLeftMargin;
rect.y += kTopMargin;
rect.height -= (kBottomMargin + kTopMargin);
// Need to know which button, if any, to draw as pressed.
const MouseCaptureEnum mouseCapture =
mMouseCapture ? mMouseCapture
// This public global variable is a hack for now, which should go away
// when TrackPanelCell gets a virtual function into which we move this
// drawing code.
: MouseCaptureEnum(TrackControls::gCaptureState);
const bool captured = (t == mCapturedTrack || t == mpClickedTrack);
TrackInfo::DrawItems( dc, rect, *t, mouseCapture, captured );
//mTrackInfo.DrawBordersWithin( dc, rect, *t );
}
// Given rectangle should be the whole track rectangle
// Paint the inset areas left, top, and right in a background color
// If linked to a following channel, also paint the separator area, which
// overlaps the next track rectangle's top
void TrackPanel::DrawOutsideOfTrack(Track * t, wxDC * dc, const wxRect & rect)
{
// Fill in area outside of the track
AColor::TrackPanelBackground(dc, false);
wxRect side;
// Area between panel border and left track border
side = rect;
side.width = kLeftInset;
dc->DrawRectangle(side);
// Area between panel border and top track border
side = rect;
side.height = kTopInset;
dc->DrawRectangle(side);
// Area between panel border and right track border
side = rect;
side.x += side.width - kTopInset;
side.width = kTopInset;
dc->DrawRectangle(side);
// Area between tracks of stereo group
if (t->GetLinked()
#ifdef EXPERIMENTAL_OUTPUT_DISPLAY
|| MONO_WAVE_PAN(t)
#endif
) {
// Paint the channel separator over (what would be) the shadow of the top
// channel, and the top inset of the bottom channel
side = rect;
side.y += t->GetHeight() - kShadowThickness;
side.height = kTopInset + kShadowThickness;
dc->DrawRectangle(side);
}
}
void TrackPanel::SetBackgroundCell
(const std::shared_ptr< TrackPanelCell > &pCell)
{
mpBackground = pCell;
}
/// Draw a three-level highlight gradient around the focused track.
void TrackPanel::HighlightFocusedTrack(wxDC * dc, const wxRect & rect)
{
wxRect theRect = rect;
theRect.x += kLeftInset;
theRect.y += kTopInset;
theRect.width -= kLeftInset * 2;
theRect.height -= kTopInset;
dc->SetBrush(*wxTRANSPARENT_BRUSH);
AColor::TrackFocusPen(dc, 0);
dc->DrawRectangle(theRect.x - 1, theRect.y - 1, theRect.width + 2, theRect.height + 2);
AColor::TrackFocusPen(dc, 1);
dc->DrawRectangle(theRect.x - 2, theRect.y - 2, theRect.width + 4, theRect.height + 4);
AColor::TrackFocusPen(dc, 2);
dc->DrawRectangle(theRect.x - 3, theRect.y - 3, theRect.width + 6, theRect.height + 6);
}
void TrackPanel::UpdateVRulers()
{
TrackListOfKindIterator iter(Track::Wave, GetTracks());
for (Track *t = iter.First(); t; t = iter.Next()) {
UpdateTrackVRuler(t);
}
UpdateVRulerSize();
}
void TrackPanel::UpdateVRuler(Track *t)
{
UpdateTrackVRuler(t);
UpdateVRulerSize();
}
void TrackPanel::UpdateTrackVRuler(const Track *t)
{
wxASSERT(t);
if (!t)
return;
wxRect rect(GetVRulerOffset(),
kTopMargin,
GetVRulerWidth(),
t->GetHeight() - (kTopMargin + kBottomMargin));
mTrackArtist->UpdateVRuler(t, rect);
const Track *l = t->GetLink();
if (l)
{
rect.height = l->GetHeight() - (kTopMargin + kBottomMargin);
mTrackArtist->UpdateVRuler(l, rect);
}
#ifdef EXPERIMENTAL_OUTPUT_DISPLAY
else if(MONO_WAVE_PAN(t)){
rect.height = t->GetHeight(true) - (kTopMargin + kBottomMargin);
mTrackArtist->UpdateVRuler(t, rect);
}
#endif
}
void TrackPanel::UpdateVRulerSize()
{
TrackListIterator iter(GetTracks());
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);
}
// Make sure selection edge is in view
void TrackPanel::ScrollIntoView(double pos)
{
int w;
GetTracksUsableArea( &w, NULL );
int pixel = mViewInfo->TimeToPosition(pos);
if (pixel < 0 || pixel >= w)
{
mListener->TP_ScrollWindow
(mViewInfo->OffsetTimeByPixels(pos, -(w / 2)));
Refresh(false);
}
}
void TrackPanel::ScrollIntoView(int x)
{
ScrollIntoView(mViewInfo->PositionToTime(x, GetLeftOffset()));
}
void TrackPanel::OnTrackMenu(Track *t)
{
BuildMenusIfNeeded();
if(!t) {
t = GetFocusedTrack();
if(!t)
return;
}
{
TrackPanelCell *const pCell = t->GetTrackControl();
const wxRect rect(FindTrackRect(t, true));
const UIHandle::Result refreshResult =
pCell->DoContextMenu(rect, this, NULL);
ProcessUIHandleResult(this, mRuler, t, t, refreshResult);
// TODO: Hide following lines inside the above.
}
mPopupMenuTarget = t;
Track *next = mTracks->GetNext(t);
wxMenu *theMenu = NULL;
if (t->GetKind() == Track::Time) {
theMenu = mTimeTrackMenu.get();
TimeTrack *tt = (TimeTrack*) t;
theMenu->Check(OnTimeTrackLogIntID, tt->GetInterpolateLog());
}
if (t->GetKind() == Track::Wave) {
theMenu = mWaveTrackMenu.get();
const bool isMono = !t->GetLinked();
const bool canMakeStereo =
(next && isMono && !next->GetLinked() &&
next->GetKind() == Track::Wave);
// Unsafe to change channels during real-time preview (bug 1560)
bool unsafe = EffectManager::Get().RealtimeIsActive() && IsUnsafe();
theMenu->Enable(OnSwapChannelsID, t->GetLinked() && !unsafe);
theMenu->Enable(OnMergeStereoID, canMakeStereo && !unsafe);
theMenu->Enable(OnSplitStereoID, t->GetLinked() && !unsafe);
// Several menu items no longer needed....
#if 0
theMenu->Enable(OnSplitStereoMonoID, t->GetLinked() && !unsafe);
// We only need to set check marks. Clearing checks causes problems on Linux (bug 851)
// + Setting unchecked items to false is to get round a linux bug
switch (t->GetChannel()) {
case Track::LeftChannel:
theMenu->Check(OnChannelLeftID, true);
theMenu->Check(OnChannelRightID, false);
theMenu->Check(OnChannelMonoID, false);
break;
case Track::RightChannel:
theMenu->Check(OnChannelRightID, true);
theMenu->Check(OnChannelLeftID, false);
theMenu->Check(OnChannelMonoID, false);
break;
default:
theMenu->Check(OnChannelMonoID, true);
theMenu->Check(OnChannelLeftID, false);
theMenu->Check(OnChannelRightID, false);
}
theMenu->Enable(OnChannelMonoID, !t->GetLinked());
theMenu->Enable(OnChannelLeftID, !t->GetLinked());
theMenu->Enable(OnChannelRightID, !t->GetLinked());
#endif
WaveTrack *const track = (WaveTrack *)t;
const int display = track->GetDisplay();
theMenu->Check(
(display == WaveTrack::Waveform)
? (track->GetWaveformSettings().isLinear() ? OnWaveformID : OnWaveformDBID)
: OnSpectrumID,
true
);
// Bug 1253. Shouldn't open preferences if audio is busy.
// We can't change them on the fly yet anyway.
const bool bAudioBusy = gAudioIO->IsBusy();
theMenu->Enable(OnSpectrogramSettingsID,
(display == WaveTrack::Spectrum) && !bAudioBusy);
SetMenuCheck(*mRateMenu, IdOfRate((int) track->GetRate()));
SetMenuCheck(*mFormatMenu, IdOfFormat(track->GetSampleFormat()));
unsafe = IsUnsafe();
for (int i = OnRate8ID; i <= OnFloatID; i++) {
theMenu->Enable(i, !unsafe);
}
}
#if defined(USE_MIDI)
if (t->GetKind() == Track::Note)
theMenu = mNoteTrackMenu.get();
#endif
if (t->GetKind() == Track::Label){
theMenu = mLabelTrackMenu.get();
}
if (theMenu) {
//We need to find the location of the menu rectangle.
const wxRect rect = FindTrackRect(t,true);
wxRect titleRect;
mTrackInfo.GetTitleBarRect(rect, 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.get();
const int id =
OnFirstWaveformScaleID + (int)(wt->GetWaveformSettings().scaleType);
theMenu->Check(id, true);
}
else {
theMenu = mRulerSpectrumMenu.get();
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 rect = FindTrackRect(wt, true);
wxRect titleRect;
mTrackInfo.GetTitleBarRect(rect, 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;
}
Track * TrackPanel::GetFirstSelectedTrack()
{
TrackListIterator iter(GetTracks());
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(GetTracks());
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);
}
// Given rectangle excludes the insets left, right, and top
// Draw a rectangular border and also a vertical separator of track controls
// from the rest (ruler and proper track area)
void TrackPanel::DrawBordersAroundTrack(Track * t, wxDC * dc,
const wxRect & rect, const int labelw,
const int vrul)
{
// Border around track and label area
// leaving room for the shadow
dc->SetBrush(*wxTRANSPARENT_BRUSH);
dc->SetPen(*wxBLACK_PEN);
dc->DrawRectangle(rect.x, rect.y,
rect.width - kShadowThickness,
rect.height - kShadowThickness);
// between vruler and TrackInfo
AColor::Line(*dc, vrul, rect.y, vrul, rect.y + rect.height - 1);
// The lines at bottom of 1st track and top of second track of stereo group
// Possibly replace with DrawRectangle to add left border.
if (t->GetLinked()
#ifdef EXPERIMENTAL_OUTPUT_DISPLAY
|| MONO_WAVE_PAN(t)
#endif
) {
// The given rect has had the top inset subtracted
int h1 = rect.y + t->GetHeight() - kTopInset;
// h1 is the top coordinate of the second tracks' rectangle
// Draw (part of) the bottom border of the top channel and top border of the bottom
// At left it extends between the vertical rulers too
// These lines stroke over what is otherwise "border" of each channel
AColor::Line(*dc, labelw, h1 - kBottomMargin, rect.x + rect.width - 1, h1 - kBottomMargin);
AColor::Line(*dc, labelw, h1 + kTopInset, rect.x + rect.width - 1, h1 + kTopInset);
}
}
// Given rectangle has insets subtracted left, right, and top
// Stroke lines along bottom and right, which are slightly short at
// bottom-left and top-right
void TrackPanel::DrawShadow(Track * /* t */ , wxDC * dc, const wxRect & rect)
{
int right = rect.x + rect.width - 1;
int bottom = rect.y + rect.height - 1;
// shadow color for lines
dc->SetPen(*wxBLACK_PEN);
// bottom
AColor::Line(*dc, rect.x, bottom, right, bottom);
// right
AColor::Line(*dc, right, rect.y, right, bottom);
// background color erases small parts of those lines
AColor::Dark(dc, false);
// bottom-left
AColor::Line(*dc, rect.x, bottom, rect.x + 1, bottom);
// top-right
AColor::Line(*dc, right, rect.y, right, rect.y + 1);
}
/// 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();
Track *const focused = GetFocusedTrack();
const bool hasFocus =
(focused == mPopupMenuTarget || focused == partner);
SplitStereo(false);
mPopupMenuTarget->SetChannel(Track::RightChannel);
partner->SetChannel(Track::LeftChannel);
(mTracks->MoveUp(partner));
partner->SetLinked(true);
MixerBoard* pMixerBoard = this->GetMixerBoard();
if (pMixerBoard) {
pMixerBoard->UpdateTrackClusters();
}
if (hasFocus)
SetFocusedTrack(partner);
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->SetPanFromChannelType();
}
mPopupMenuTarget->SetChannel(Track::MonoChannel);
// Assume partner is present, and is wave
auto partner = static_cast<WaveTrack*>(mPopupMenuTarget->GetLink());
wxASSERT(partner);
if (!partner)
return;
#ifdef EXPERIMENTAL_OUTPUT_DISPLAY
if(!stereo && MONO_WAVE_PAN(mPopupMenuTarget))
// Come here only from wave track menu
static_cast<WaveTrack*>(mPopupMenuTarget)->SetVirtualState(true,true);
if(!stereo && MONO_WAVE_PAN(partner))
partner->SetVirtualState(true,true);
#endif
if (partner)
{
partner->SetName(mPopupMenuTarget->GetName());
if (stereo){
partner->SetPanFromChannelType();
}
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(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->SetPan( 0.0f );
mPopupMenuTarget->SetChannel(Track::LeftChannel);
partner->SetPan( 0.0f );
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.
WaveTrack *wt, *pwt;
if(ODManager::IsInstanceCreated() &&
// Assume linked track is wave or null
nullptr != (pwt = static_cast<WaveTrack*>(partner)) &&
// Come here only from the wave track menu
nullptr != (wt = static_cast<WaveTrack*>(mPopupMenuTarget)))
if(!ODManager::Instance()->MakeWaveTrackDependent(pwt, wt))
{
;
//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 final : public PrefsDialog
{
public:
ViewSettingsDialog
(wxWindow *parent, const wxString &title, PrefsDialog::Factories &factories,
int page)
: PrefsDialog(parent, title, factories)
, mPage(page)
{
}
long GetPreferredPage() override
{
return mPage;
}
void SavePreferredPage() override
{
}
private:
const int mPage;
};
void TrackPanel::OnSpectrogramSettings(wxCommandEvent &)
{
if (gAudioIO->IsBusy()){
wxMessageBox(_("To change Spectrogram Settings, stop any\n."
"playing or recording first."),
_("Stop the Audio First"), wxOK | wxICON_EXCLAMATION | wxCENTRE);
return;
}
// Get here only from the wave track menu
const auto wt = static_cast<WaveTrack*>(mPopupMenuTarget);
// WaveformPrefsFactory waveformFactory(wt);
//TracksBehaviorsPrefsFactory tracksBehaviorsFactory();
SpectrumPrefsFactory spectrumFactory(wt);
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()) {
MakeParentModifyState(true);
// 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);
bool linear = false;
WaveTrack::WaveTrackDisplay id;
switch (idInt) {
default:
case OnWaveformID:
linear = true, id = WaveTrack::Waveform; break;
case OnWaveformDBID:
id = WaveTrack::Waveform; break;
case OnSpectrumID:
id = WaveTrack::Spectrum; break;
}
WaveTrack *wt = (WaveTrack *) mPopupMenuTarget;
const bool wrongType = wt->GetDisplay() != id;
const bool wrongScale =
(id == WaveTrack::Waveform &&
wt->GetWaveformSettings().isLinear() != linear);
if (wrongType || wrongScale) {
wt->SetLastScaleType();
wt->SetDisplay(WaveTrack::WaveTrackDisplay(id));
if (wrongScale)
wt->GetIndependentWaveformSettings().scaleType = linear
? WaveformSettings::stLinear
: WaveformSettings::stLogarithmic;
// Assume linked track is wave or null
const auto l = static_cast<WaveTrack*>(wt->GetLink());
if (l) {
l->SetLastScaleType();
l->SetDisplay(WaveTrack::WaveTrackDisplay(id));
if (wrongScale)
l->GetIndependentWaveformSettings().scaleType = linear
? WaveformSettings::stLinear
: WaveformSettings::stLogarithmic;
}
#ifdef EXPERIMENTAL_OUTPUT_DISPLAY
if (wt->GetDisplay() == WaveTrack::Waveform) {
wt->SetVirtualState(false);
}else if (id == WaveTrack::Waveform) {
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(WaveTrack * wt, double rate)
{
wt->SetRate(rate);
// Assume linked track is wave or null
const auto partner = static_cast<WaveTrack*>(wt->GetLink());
if (partner)
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"),
wt->GetName().c_str(), rateString.c_str()),
_("Rate Change"));
}
/// Handles the selection from the Format submenu of the
/// track menu.
void TrackPanel::OnFormatChange(wxCommandEvent & event)
{
BuildMenusIfNeeded();
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.
((WaveTrack*)mPopupMenuTarget)->ConvertToSampleFormat(newFormat);
// Assume linked track is wave or null
const auto partner =
static_cast<WaveTrack*>(mPopupMenuTarget->GetLink());
if (partner)
partner->ConvertToSampleFormat(newFormat);
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 )
{
auto & list = menu.GetMenuItems();
for ( auto item : list )
{
auto 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)
{
BuildMenusIfNeeded();
int id = event.GetId();
wxASSERT(id >= OnRate8ID && id <= OnRate384ID);
SetMenuCheck( *mRateMenu, id );
// Come here only from wave track menu
SetRate(static_cast<WaveTrack*>(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)
{
BuildMenusIfNeeded();
// Come here only from the wave track menu
const auto wt = static_cast<WaveTrack*>(mPopupMenuTarget);
wxASSERT(wt);
int newRate;
/// \todo Remove artificial constants!!
/// \todo Make a real dialog box out of this!!
while (true)
{
wxDialogWrapper 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(wt, 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*/)
{
// Come here only from the time track menu
const auto t = static_cast<TimeTrack*>(mPopupMenuTarget);
t->SetDisplayLog(false);
UpdateVRuler(t);
MakeParentPushState(_("Set time track display to linear"), _("Set Display"));
Refresh(false);
}
void TrackPanel::OnTimeTrackLog(wxCommandEvent & /*event*/)
{
// Come here only from the time track menu
const auto t = static_cast<TimeTrack*>(mPopupMenuTarget);
t->SetDisplayLog(true);
UpdateVRuler(t);
MakeParentPushState(_("Set time track display to logarithmic"), _("Set Display"));
Refresh(false);
}
void TrackPanel::OnTimeTrackLogInt(wxCommandEvent & /*event*/)
{
// Come here only from the time track menu
const auto t = static_cast<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)
{
// Get here only from vertical ruler menu for wave tracks
const auto wt = static_cast<WaveTrack *>(mPopupMenuTarget);
// Assume linked track is wave or null
const auto partner = static_cast<WaveTrack*>(wt->GetLink());
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;
if (partner)
partner->GetIndependentWaveformSettings().scaleType = newScaleType;
UpdateVRuler(wt); // Is this really needed?
MakeParentModifyState(true);
Refresh(false);
}
}
void TrackPanel::OnSpectrumScaleType(wxCommandEvent &evt)
{
// Get here only from vertical ruler menu for wave tracks
const auto wt = static_cast<WaveTrack *>(mPopupMenuTarget);
// Assume linked track is wave or null
const auto partner = static_cast<WaveTrack*>(wt->GetLink());
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;
if (partner)
partner->GetIndependentSpectrogramSettings().scaleType = newScaleType;
UpdateVRuler(wt); // Is this really needed?
MakeParentModifyState(true);
Refresh(false);
}
}
void TrackPanel::OnZoomInVertical(wxCommandEvent &)
{
// Get here only from vertical ruler menu for wave tracks
HandleWaveTrackVZoom(static_cast<WaveTrack*>(mPopupMenuTarget), false, false);
}
void TrackPanel::OnZoomOutVertical(wxCommandEvent &)
{
// Get here only from vertical ruler menu for wave tracks
HandleWaveTrackVZoom(static_cast<WaveTrack*>(mPopupMenuTarget), true, false);
}
void TrackPanel::OnZoomFitVertical(wxCommandEvent &)
{
// Get here only from vertical ruler menu for wave tracks
HandleWaveTrackVZoom(static_cast<WaveTrack*>(mPopupMenuTarget), true, true);
}
/// 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
}
// 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 final : public wxFontEnumerator
{
public:
TrackPanelFontEnumerator(wxArrayString* fontNames) :
mFontNames(fontNames) {}
bool OnFacename(const wxString& font) override
{
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(""));
// Correct for empty facename, or bad preference file:
// get the name of a really existing font, to highlight by default
// in the list box
facename = LabelTrack::GetFont(facename).GetFaceName();
long fontsize = gPrefs->Read(wxT("/GUI/LabelFontSize"),
LabelTrack::DefaultFontSize);
/* i18n-hint: (noun) This is the font for the label track.*/
wxDialogWrapper 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 = safenew 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 = safenew 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 cell is under the mouse
/// @param mouseX - mouse X position.
/// @param mouseY - mouse Y position.
TrackPanel::FoundCell TrackPanel::FindCell(int mouseX, int mouseY)
{
auto size = GetSize();
size.x -= kRightMargin;
wxRect rect { 0, 0, 0, 0 };
// The type of cell that may be found is determined by the x coordinate.
CellType type = CellType::Track;
if (mouseX < kLeftMargin)
;
else if (mouseX < GetVRulerOffset())
type = CellType::Label,
rect.x = kLeftMargin,
rect.width = GetVRulerOffset() - kLeftMargin;
else if (mouseX < GetLeftOffset())
type = CellType::VRuler,
rect.x = GetVRulerOffset(),
rect.width = GetLeftOffset() - GetVRulerOffset();
else if (mouseX < size.x)
type = CellType::Track,
rect.x = GetLeftOffset(),
rect.width = size.x - GetLeftOffset();
auto output = [&](Track *pTrack) -> FoundCell {
// Undo the bias mentioned below.
rect.y -= kTopMargin;
TrackPanelCell *pCell {};
if (pTrack) switch (type) {
case CellType::Label:
pCell = pTrack->GetTrackControl(); break;
case CellType::VRuler:
pCell = pTrack->GetVRulerControl(); break;
default:
pCell = pTrack; break;
}
if (pTrack)
return { pTrack, pCell, type, rect };
else
return { nullptr, nullptr, type, {} };
};
VisibleTrackIterator iter(GetProject());
for (Track * t = iter.First(); t; t = iter.Next()) {
// The zone to hit the track is biased to exclude the margin above
// but include the top margin of the track below. That makes the change
// to the track resizing cursor work right.
rect.y = t->GetY() - mViewInfo->vpos + kTopMargin;
rect.height = t->GetHeight();
if (type == CellType::Label) {
if (t->GetLink()) {
Track *l = t->GetLink();
int h = l->GetHeight();
if (!t->GetLinked()) {
t = l;
rect.y = t->GetY() - mViewInfo->vpos + kTopMargin;
}
rect.height += h;
}
#ifdef EXPERIMENTAL_OUTPUT_DISPLAY
else if( MONO_WAVE_PAN(t) )
rect.height += t->GetHeight(true);
#endif
}
//Determine whether the mouse is inside
//the current rectangle. If so, return.
if (rect.Contains(mouseX, mouseY)) {
#ifdef EXPERIMENTAL_OUTPUT_DISPLAY
// PRL: Is it good to have a side effect in a hit-testing routine?
t->SetVirtualStereo(false);
#endif
return output(t);
}
#ifdef EXPERIMENTAL_OUTPUT_DISPLAY
if(type != CellType::Label && MONO_WAVE_PAN(t)){
rect.y = t->GetY(true) - mViewInfo->vpos + kTopMargin;
rect.height = t->GetHeight(true);
if (rect.Contains(mouseX, mouseY)) {
// PRL: Is it good to have a side effect in a hit-testing routine?
t->SetVirtualStereo(true);
return output(t);
}
}
#endif // EXPERIMENTAL_OUTPUT_DISPLAY
}
if (mpBackground) {
// In default of hits on any other cells
// Find a disjoint, maybe empty, rectangle
// for the empty space appearing at bottom
rect.x = kLeftMargin;
rect.width = size.x - rect.x;
rect.y =
std::min( size.y,
std::max( 0,
rect.y - kTopMargin + rect.height ) );
rect.height = size.y - rect.y;
GetSize(&rect.width, &rect.height);
return {
nullptr, mpBackground.get(),
CellType::Background, rect
};
}
else
return { nullptr, nullptr, type, {} };
}
/// This finds the rectangle of a given track, either the
/// of the label 'adornment' or the track itself
wxRect TrackPanel::FindTrackRect( const Track * target, bool label )
{
if (!target) {
return { 0, 0, 0, 0 };
}
wxRect rect{
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()) {
rect.height += target->GetLink()->GetHeight();
}
rect.x += kLeftMargin;
if (label)
rect.width = GetVRulerOffset() - kLeftMargin;
else
rect.width -= (kLeftMargin + kRightMargin);
rect.y += kTopMargin;
rect.height -= (kTopMargin + kBottomMargin);
return rect;
}
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();
}
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 (t && AudacityProject::GetKeyboardCaptureHandler()) {
AudacityProject::ReleaseKeyboard(this);
}
if (t) {
AudacityProject::CaptureKeyboard(this);
}
mAx->SetFocus( t );
Refresh( false );
}
void TrackPanel::OnSetFocus(wxFocusEvent & WXUNUSED(event))
{
SetFocusedTrack( GetFocusedTrack() );
Refresh( false );
}
void TrackPanel::OnKillFocus(wxFocusEvent & WXUNUSED(event))
{
if (AudacityProject::HasKeyboardCapture(this))
{
AudacityProject::ReleaseKeyboard(this);
}
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;
ReCreateSliders();
UpdatePrefs();
}
TrackInfo::~TrackInfo()
{
}
void TrackInfo::ReCreateSliders(){
const wxPoint point{ 0, 0 };
wxRect sliderRect;
GetGainRect(point, sliderRect);
float defPos = 1.0;
/* i18n-hint: Title of the Gain slider, used to adjust the volume */
gGain = std::make_unique<LWSlider>(pParent, _("Gain"),
wxPoint(sliderRect.x, sliderRect.y),
wxSize(sliderRect.width, sliderRect.height),
DB_SLIDER);
gGain->SetDefaultValue(defPos);
gGainCaptured = std::make_unique<LWSlider>(pParent, _("Gain"),
wxPoint(sliderRect.x, sliderRect.y),
wxSize(sliderRect.width, sliderRect.height),
DB_SLIDER);
gGainCaptured->SetDefaultValue(defPos);
GetPanRect(point, sliderRect);
defPos = 0.0;
/* i18n-hint: Title of the Pan slider, used to move the sound left or right */
gPan = std::make_unique<LWSlider>(pParent, _("Pan"),
wxPoint(sliderRect.x, sliderRect.y),
wxSize(sliderRect.width, sliderRect.height),
PAN_SLIDER);
gPan->SetDefaultValue(defPos);
gPanCaptured = std::make_unique<LWSlider>(pParent, _("Pan"),
wxPoint(sliderRect.x, sliderRect.y),
wxSize(sliderRect.width, sliderRect.height),
PAN_SLIDER);
gPanCaptured->SetDefaultValue(defPos);
#ifdef EXPERIMENTAL_MIDI_OUT
GetVelocityRect(point, sliderRect);
/* i18n-hint: Title of the Velocity slider, used to adjust the volume of note tracks */
gVelocity = std::make_unique<LWSlider>(pParent, _("Velocity"),
wxPoint(sliderRect.x, sliderRect.y),
wxSize(sliderRect.width, sliderRect.height),
VEL_SLIDER);
gVelocity->SetDefaultValue(0.0);
gVelocityCaptured = std::make_unique<LWSlider>(pParent, _("Velocity"),
wxPoint(sliderRect.x, sliderRect.y),
wxSize(sliderRect.width, sliderRect.height),
VEL_SLIDER);
gVelocityCaptured->SetDefaultValue(0.0);
#endif
}
int TrackInfo::GetTrackInfoWidth() const
{
return kTrackInfoWidth;
}
void TrackInfo::GetCloseBoxHorizontalBounds( const wxRect & rect, wxRect &dest )
{
dest.x = rect.x;
dest.width = kTrackInfoBtnSize;
}
void TrackInfo::GetCloseBoxRect(const wxRect & rect, wxRect & dest)
{
GetCloseBoxHorizontalBounds( rect, dest );
auto results = CalcItemY( commonTrackTCPLines, kItemBarButtons );
dest.y = rect.y + results.first;
dest.height = results.second;
}
static const int TitleSoloBorderOverlap = 1;
void TrackInfo::GetTitleBarHorizontalBounds( const wxRect & rect, wxRect &dest )
{
// to right of CloseBoxRect, plus a little more
wxRect closeRect;
GetCloseBoxHorizontalBounds( rect, closeRect );
dest.x = rect.x + closeRect.width + 1;
dest.width = rect.x + rect.width - dest.x + TitleSoloBorderOverlap;
}
void TrackInfo::GetTitleBarRect(const wxRect & rect, wxRect & dest)
{
GetTitleBarHorizontalBounds( rect, dest );
auto results = CalcItemY( commonTrackTCPLines, kItemBarButtons );
dest.y = rect.y + results.first;
dest.height = results.second;
}
void TrackInfo::GetNarrowMuteHorizontalBounds( const wxRect & rect, wxRect &dest )
{
dest.x = rect.x;
dest.width = rect.width / 2 + 1;
}
void TrackInfo::GetNarrowSoloHorizontalBounds( const wxRect & rect, wxRect &dest )
{
wxRect muteRect;
GetNarrowMuteHorizontalBounds( rect, muteRect );
dest.x = rect.x + muteRect.width;
dest.width = rect.width - muteRect.width + TitleSoloBorderOverlap;
}
void TrackInfo::GetWideMuteSoloHorizontalBounds( const wxRect & rect, wxRect &dest )
{
// Larger button, symmetrically placed intended.
// On windows this gives 15 pixels each side.
dest.width = rect.width - 2 * kTrackInfoBtnSize + 6;
dest.x = rect.x + kTrackInfoBtnSize -3;
}
void TrackInfo::GetMuteSoloRect
(const wxRect & rect, wxRect & dest, bool solo, bool bHasSoloButton,
const Track *pTrack)
{
auto resultsM = CalcItemY( getTCPLines( *pTrack ), kItemMute );
auto resultsS = CalcItemY( getTCPLines( *pTrack ), kItemSolo );
dest.height = resultsS.second;
int yMute = resultsM.first;
int ySolo = resultsS.first;
bool bSameRow = ( yMute == ySolo );
bool bNarrow = bSameRow && bHasSoloButton;
if( bNarrow )
{
if( solo )
GetNarrowSoloHorizontalBounds( rect, dest );
else
GetNarrowMuteHorizontalBounds( rect, dest );
}
else
GetWideMuteSoloHorizontalBounds( rect, dest );
if( bSameRow || !solo )
dest.y = rect.y + yMute;
else
dest.y = rect.y + ySolo;
}
void TrackInfo::GetSliderHorizontalBounds( const wxPoint &topleft, wxRect &dest )
{
dest.x = topleft.x + 6;
dest.width = kTrackInfoSliderWidth;
}
void TrackInfo::GetGainRect(const wxPoint &topleft, wxRect & dest)
{
GetSliderHorizontalBounds( topleft, dest );
auto results = CalcItemY( waveTrackTCPLines, kItemGain );
dest.y = topleft.y + results.first;
dest.height = results.second;
}
void TrackInfo::GetPanRect(const wxPoint &topleft, wxRect & dest)
{
GetGainRect( topleft, dest );
auto results = CalcItemY( waveTrackTCPLines, kItemPan );
dest.y = topleft.y + results.first;
}
#ifdef EXPERIMENTAL_MIDI_OUT
void TrackInfo::GetVelocityRect(const wxPoint &topleft, wxRect & dest)
{
GetSliderHorizontalBounds( topleft, dest );
auto results = CalcItemY( noteTrackTCPLines, kItemVelocity );
dest.y = topleft.y + results.first;
dest.height = results.second;
}
#endif
void TrackInfo::GetMinimizeHorizontalBounds( const wxRect &rect, wxRect &dest )
{
const int space = 0;// was 3.
dest.x = rect.x + space;
wxRect syncLockRect;
GetSyncLockHorizontalBounds( rect, syncLockRect );
// Width is rect.width less space on left for track select
// and on right for sync-lock icon.
dest.width = rect.width - (space + syncLockRect.width);
}
void TrackInfo::GetMinimizeRect(const wxRect & rect, wxRect &dest)
{
GetMinimizeHorizontalBounds( rect, dest );
auto results = CalcBottomItemY
( commonTrackTCPBottomLines, kItemMinimize, rect.height);
dest.y = rect.y + results.first;
dest.height = results.second;
}
void TrackInfo::GetSyncLockHorizontalBounds( const wxRect &rect, wxRect &dest )
{
dest.width = kTrackInfoBtnSize;
dest.x = rect.x + rect.width - dest.width;
}
void TrackInfo::GetSyncLockIconRect(const wxRect & rect, wxRect &dest)
{
GetSyncLockHorizontalBounds( rect, dest );
auto results = CalcBottomItemY
( commonTrackTCPBottomLines, kItemSyncLock, rect.height);
dest.y = rect.y + results.first;
dest.height = results.second;
}
#ifdef USE_MIDI
void TrackInfo::GetMidiControlsHorizontalBounds
( const wxRect &rect, wxRect &dest )
{
dest.x = rect.x + 1; // To center slightly
// PRL: TODO: kMidiCellWidth is defined in terms of the other constant
// kTrackInfoWidth but I am trying to avoid use of that constant.
// Can cell width be computed from dest.width instead?
dest.width = kMidiCellWidth * 4;
}
void TrackInfo::GetMidiControlsRect(const wxRect & rect, wxRect & dest)
{
GetMidiControlsHorizontalBounds( rect, dest );
auto results = CalcItemY( noteTrackTCPLines, kItemMidiControlsRect );
dest.y = rect.y + results.first;
dest.height = results.second;
}
#endif
wxFont TrackInfo::gFont;
/// \todo Probably should move to 'Utils.cpp'.
void TrackInfo::SetTrackInfoFont(wxDC * dc)
{
dc->SetFont(gFont);
}
void TrackInfo::DrawBordersWithin
( wxDC* dc, const wxRect & rect, const Track &track ) const
{
AColor::Dark(dc, false); // same color as border of toolbars (ToolBar::OnPaint())
// below close box and title bar
wxRect buttonRect;
GetTitleBarRect( rect, buttonRect );
AColor::Line
(*dc, rect.x, buttonRect.y + buttonRect.height,
rect.width - 1, buttonRect.y + buttonRect.height);
// between close box and title bar
AColor::Line
(*dc, buttonRect.x, buttonRect.y,
buttonRect.x, buttonRect.y + buttonRect.height - 1);
GetMuteSoloRect( rect, buttonRect, false, true, &track );
bool bHasMuteSolo = dynamic_cast<const PlayableTrack*>( &track );
if( bHasMuteSolo && !TrackInfo::HideTopItem( rect, buttonRect ) )
{
// above mute/solo
AColor::Line
(*dc, rect.x, buttonRect.y,
rect.width - 1, buttonRect.y);
// between mute/solo
// Draw this little line; if there is no solo, wide mute button will
// overpaint it later:
AColor::Line
(*dc, buttonRect.x + buttonRect.width, buttonRect.y,
buttonRect.x + buttonRect.width, buttonRect.y + buttonRect.height - 1);
// below mute/solo
AColor::Line
(*dc, rect.x, buttonRect.y + buttonRect.height,
rect.width - 1, buttonRect.y + buttonRect.height);
}
// left of and above minimize button
wxRect minimizeRect;
this->GetMinimizeRect(rect, minimizeRect);
AColor::Line
(*dc, minimizeRect.x - 1, minimizeRect.y,
minimizeRect.x - 1, minimizeRect.y + minimizeRect.height - 1);
AColor::Line
(*dc, minimizeRect.x, minimizeRect.y - 1,
minimizeRect.x + minimizeRect.width - 1, minimizeRect.y - 1);
}
//#define USE_BEVELS
// Paint the whole given rectangle some fill color
void TrackInfo::DrawBackground(wxDC * dc, const wxRect & rect, bool bSelected,
bool bHasMuteSolo, const int labelw, const int vrul) const
{
//compiler food.
bHasMuteSolo;
vrul;
// fill in label
wxRect fill = rect;
fill.width = labelw-4;
AColor::MediumTrackInfo(dc, bSelected);
dc->DrawRectangle(fill);
#ifdef USE_BEVELS
// This branch is not now used
// PRL: todo: banish magic numbers
if( bHasMuteSolo )
{
int ylast = rect.height-20;
int ybutton = wxMin(32,ylast-17);
int ybuttonEnd = 67;
fill=wxRect( rect.x+1, rect.y+17, vrul-6, ybutton);
AColor::BevelTrackInfo( *dc, true, fill );
if( ybuttonEnd < ylast ){
fill=wxRect( rect.x+1, rect.y+ybuttonEnd, fill.width, ylast - ybuttonEnd);
AColor::BevelTrackInfo( *dc, true, fill );
}
}
else
{
fill=wxRect( rect.x+1, rect.y+17, vrul-6, rect.height-37);
AColor::BevelTrackInfo( *dc, true, fill );
}
#endif
}
namespace {
unsigned DefaultTrackHeight( const TCPLines &topLines )
{
int needed =
kTopMargin + kBottomMargin +
totalTCPLines( topLines, true ) +
totalTCPLines( commonTrackTCPBottomLines, false ) + 1;
return (unsigned) std::max( needed, (int) Track::DefaultHeight );
}
}
unsigned TrackInfo::DefaultNoteTrackHeight()
{
return DefaultTrackHeight( noteTrackTCPLines );
}
unsigned TrackInfo::DefaultWaveTrackHeight()
{
return DefaultTrackHeight( waveTrackTCPLines );
}
std::unique_ptr<LWSlider>
TrackInfo::gGainCaptured
, TrackInfo::gPanCaptured
, TrackInfo::gGain
, TrackInfo::gPan
#ifdef EXPERIMENTAL_MIDI_OUT
, TrackInfo::gVelocityCaptured
, TrackInfo::gVelocity
#endif
;
LWSlider * TrackInfo::GainSlider
(const wxRect &sliderRect, const WaveTrack *t, bool captured, wxWindow *pParent)
{
wxPoint pos = sliderRect.GetPosition();
float gain = t ? t->GetGain() : 1.0;
gGain->Move(pos);
gGain->Set(gain);
gGainCaptured->Move(pos);
gGainCaptured->Set(gain);
auto slider = (captured ? gGainCaptured : gGain).get();
slider->SetParent( pParent ? pParent : ::GetActiveProject() );
return slider;
}
LWSlider * TrackInfo::PanSlider
(const wxRect &sliderRect, const WaveTrack *t, bool captured, wxWindow *pParent)
{
wxPoint pos = sliderRect.GetPosition();
float pan = t ? t->GetPan() : 0.0;
gPan->Move(pos);
gPan->Set(pan);
gPanCaptured->Move(pos);
gPanCaptured->Set(pan);
auto slider = (captured ? gPanCaptured : gPan).get();
slider->SetParent( pParent ? pParent : ::GetActiveProject() );
return slider;
}
#ifdef EXPERIMENTAL_MIDI_OUT
LWSlider * TrackInfo::VelocitySlider
(const wxRect &sliderRect, const NoteTrack *t, bool captured, wxWindow *pParent)
{
wxPoint pos = sliderRect.GetPosition();
float velocity = t ? t->GetVelocity() : 0.0;
gVelocity->Move(pos);
gVelocity->Set(velocity);
gVelocityCaptured->Move(pos);
gVelocityCaptured->Set(velocity);
auto slider = (captured ? gVelocityCaptured : gVelocity).get();
slider->SetParent( pParent ? pParent : ::GetActiveProject() );
return slider;
}
#endif
void TrackInfo::UpdatePrefs()
{
// Calculation of best font size depends on language, so it should be redone in case
// the language preference changed.
int fontSize = 10;
gFont.Create(fontSize, wxFONTFAMILY_SWISS, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL);
int allowableWidth = GetTrackInfoWidth() - 2; // 2 to allow for left/right borders
int textWidth, textHeight;
do {
gFont.SetPointSize(fontSize);
pParent->GetTextExtent(_("Stereo, 999999Hz"),
&textWidth,
&textHeight,
NULL,
NULL,
&gFont);
fontSize--;
} while (textWidth >= allowableWidth);
}
TrackPanelCellIterator::TrackPanelCellIterator(TrackPanel *trackPanel, bool begin)
: mPanel(trackPanel)
, mIter(trackPanel->GetProject())
, mpCell(begin ? mIter.First() : NULL)
{
}
TrackPanelCellIterator &TrackPanelCellIterator::operator++ ()
{
// To do, iterate over the other cells that are not tracks
mpCell = mIter.Next();
return *this;
}
TrackPanelCellIterator TrackPanelCellIterator::operator++ (int)
{
TrackPanelCellIterator copy(*this);
++ *this;
return copy;
}
auto TrackPanelCellIterator::operator* () const -> value_type
{
Track *const pTrack = dynamic_cast<Track*>(mpCell);
if (!pTrack)
// to do: handle cells that are not tracks
return std::make_pair((Track*)nullptr, wxRect());
// Convert virtual coordinate to physical
int width;
mPanel->GetTracksUsableArea(&width, NULL);
int y = pTrack->GetY() - mPanel->GetViewInfo()->vpos;
return std::make_pair(
mpCell,
wxRect(
mPanel->GetLeftOffset(),
y + kTopMargin,
width,
pTrack->GetHeight() - (kTopMargin + kBottomMargin)
)
);
}
static TrackPanel * TrackPanelFactory(wxWindow * parent,
wxWindowID id,
const wxPoint & pos,
const wxSize & size,
const std::shared_ptr<TrackList> &tracks,
ViewInfo * viewInfo,
TrackPanelListener * listener,
AdornedRulerPanel * ruler)
{
wxASSERT(parent); // to justify safenew
return safenew 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,
const std::shared_ptr<TrackList> &tracks,
ViewInfo * viewInfo,
TrackPanelListener * listener,
AdornedRulerPanel * ruler) = TrackPanelFactory;
TrackPanelCell::~TrackPanelCell()
{
}
unsigned TrackPanelCell::HandleWheelRotation
(const TrackPanelMouseEvent &, AudacityProject *)
{
return RefreshCode::Cancelled;
}
unsigned TrackPanelCell::DoContextMenu
(const wxRect &, wxWindow*, wxPoint *)
{
return RefreshCode::RefreshNone;
}
unsigned TrackPanelCell::CaptureKey(wxKeyEvent &event, ViewInfo &, wxWindow *)
{
event.Skip();
return RefreshCode::RefreshNone;
}
unsigned TrackPanelCell::KeyDown(wxKeyEvent &event, ViewInfo &, wxWindow *)
{
event.Skip();
return RefreshCode::RefreshNone;
}
unsigned TrackPanelCell::KeyUp(wxKeyEvent &event, ViewInfo &, wxWindow *)
{
event.Skip();
return RefreshCode::RefreshNone;
}
unsigned TrackPanelCell::Char(wxKeyEvent &event, ViewInfo &, wxWindow *)
{
event.Skip();
return RefreshCode::RefreshNone;
}