audacia/src/menus/SelectMenus.cpp

1393 lines
42 KiB
C++

#include "../Audacity.h"
#include "../Experimental.h"
#include "../AdornedRulerPanel.h"
#include "../AudioIO.h"
#include "../FreqWindow.h"
#include "../Menus.h" // for PrefsListener
#include "../Prefs.h"
#include "../Project.h"
#include "../TimeDialog.h"
#include "../TrackPanel.h"
#include "../ViewInfo.h"
#include "../WaveTrack.h"
#include "../NoteTrack.h"
#include "../commands/CommandContext.h"
#include "../commands/CommandManager.h"
#include "../toolbars/ControlToolBar.h"
#include "../tracks/ui/SelectHandle.h"
// private helper classes and functions
namespace {
void DoSelectTimeAndTracks
(AudacityProject &project, bool bAllTime, bool bAllTracks)
{
auto &tracks = TrackList::Get( project );
auto trackPanel = project.GetTrackPanel();
auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;
if( bAllTime )
selectedRegion.setTimes(
tracks.GetMinOffset(), tracks.GetEndTime());
if( bAllTracks ) {
for (auto t : tracks.Any())
t->SetSelected(true);
project.ModifyState(false);
trackPanel->Refresh(false);
}
}
// Temporal selection (not TimeTrack selection)
// potentially for all wave tracks.
void DoSelectTimeAndAudioTracks
(AudacityProject &project, bool bAllTime, bool bAllTracks)
{
auto &tracks = TrackList::Get( project );
auto trackPanel = project.GetTrackPanel();
auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;
if( bAllTime )
selectedRegion.setTimes(
tracks.GetMinOffset(), tracks.GetEndTime());
if( bAllTracks ) {
// Unselect all tracks before selecting audio.
for (auto t : tracks.Any())
t->SetSelected(false);
for (auto t : tracks.Any<WaveTrack>())
t->SetSelected(true);
project.ModifyState(false);
trackPanel->Refresh(false);
}
}
void DoNextPeakFrequency(AudacityProject &project, bool up)
{
auto &tracks = TrackList::Get( project );
auto &viewInfo = ViewInfo::Get( project );
auto trackPanel = project.GetTrackPanel();
// Find the first selected wave track that is in a spectrogram view.
const WaveTrack *pTrack {};
for ( auto wt : tracks.Selected< const WaveTrack >() ) {
const int display = wt->GetDisplay();
if (display == WaveTrack::Spectrum) {
pTrack = wt;
break;
}
}
if (pTrack) {
SpectrumAnalyst analyst;
SelectHandle::SnapCenterOnce(analyst, viewInfo, pTrack, up);
trackPanel->Refresh(false);
project.ModifyState(false);
}
}
double NearestZeroCrossing
(AudacityProject &project, double t0)
{
auto rate = project.GetRate();
auto &tracks = TrackList::Get( project );
// Window is 1/100th of a second.
auto windowSize = size_t(std::max(1.0, rate / 100));
Floats dist{ windowSize, true };
int nTracks = 0;
for (auto one : tracks.Selected< const WaveTrack >()) {
auto oneWindowSize = size_t(std::max(1.0, one->GetRate() / 100));
Floats oneDist{ oneWindowSize };
auto s = one->TimeToLongSamples(t0);
// fillTwo to ensure that missing values are treated as 2, and hence do
// not get used as zero crossings.
one->Get((samplePtr)oneDist.get(), floatSample,
s - (int)oneWindowSize/2, oneWindowSize, fillTwo);
// Looking for actual crossings.
double prev = 2.0;
for(size_t i=0; i<oneWindowSize; i++){
float fDist = fabs( oneDist[i]); // score is absolute value
if( prev * oneDist[i] > 0 ) // both same sign? No good.
fDist = fDist + 0.4; // No good if same sign.
else if( prev > 0.0 )
fDist = fDist + 0.1; // medium penalty for downward crossing.
prev = oneDist[i];
oneDist[i] = fDist;
}
// TODO: The mixed rate zero crossing code is broken,
// if oneWindowSize > windowSize we'll miss out some
// samples - so they will still be zero, so we'll use them.
for(size_t i = 0; i < windowSize; i++) {
size_t j;
if (windowSize != oneWindowSize)
j = i * (oneWindowSize-1) / (windowSize-1);
else
j = i;
dist[i] += oneDist[j];
// Apply a small penalty for distance from the original endpoint
// We'll always prefer an upward
dist[i] +=
0.1 * (abs(int(i) - int(windowSize/2))) / float(windowSize/2);
}
nTracks++;
}
// Find minimum
int argmin = 0;
float min = 3.0;
for(size_t i=0; i<windowSize; i++) {
if (dist[i] < min) {
argmin = i;
min = dist[i];
}
}
// If we're worse than 0.2 on average, on one track, then no good.
if(( nTracks == 1 ) && ( min > (0.2*nTracks) ))
return t0;
// If we're worse than 0.6 on average, on multi-track, then no good.
if(( nTracks > 1 ) && ( min > (0.6*nTracks) ))
return t0;
return t0 + (argmin - (int)windowSize/2) / rate;
}
// If this returns true, then there was a key up, and nothing more to do,
// after this function has completed.
// (at most this function just does a ModifyState for the keyup)
bool OnlyHandleKeyUp( const CommandContext &context )
{
auto &project = context.project;
auto evt = context.pEvt;
bool bKeyUp = (evt) && evt->GetEventType() == wxEVT_KEY_UP;
if( project.IsAudioActive() )
return bKeyUp;
if( !bKeyUp )
return false;
project.ModifyState(false);
return true;
}
enum CursorDirection {
DIRECTION_LEFT = -1,
DIRECTION_RIGHT = +1
};
enum SelectionOperation {
SELECTION_EXTEND,
SELECTION_CONTRACT,
CURSOR_MOVE
};
enum TimeUnit {
TIME_UNIT_SECONDS,
TIME_UNIT_PIXELS
};
struct SeekInfo
{
wxLongLong mLastSelectionAdjustment { ::wxGetUTCTimeMillis() };
double mSeekShort{ 0.0 };
double mSeekLong{ 0.0 };
};
void SeekWhenAudioActive(double seekStep, wxLongLong &lastSelectionAdjustment)
{
#ifdef EXPERIMENTAL_IMPROVED_SEEKING
if (gAudioIO->GetLastPlaybackTime() < lastSelectionAdjustment) {
// Allow time for the last seek to output a buffer before
// discarding samples again
// Do not advance mLastSelectionAdjustment
return;
}
#endif
lastSelectionAdjustment = ::wxGetUTCTimeMillis();
gAudioIO->SeekStream(seekStep);
}
// Handles moving a selection edge with the keyboard in snap-to-time mode;
// returns the moved value.
// Will move at least minPix pixels -- set minPix positive to move forward,
// negative to move backward.
// Helper for moving by keyboard with snap-to-grid enabled
double GridMove
(AudacityProject &project, double t, int minPix)
{
auto rate = project.GetRate();
auto &viewInfo = ViewInfo::Get( project );
auto format = project.GetSelectionFormat();
NumericConverter nc(NumericConverter::TIME, format, t, rate);
// Try incrementing/decrementing the value; if we've moved far enough we're
// done
double result;
minPix >= 0 ? nc.Increment() : nc.Decrement();
result = nc.GetValue();
if (std::abs(viewInfo.TimeToPosition(result) - viewInfo.TimeToPosition(t))
>= abs(minPix))
return result;
// Otherwise, move minPix pixels, then snap to the time.
result = viewInfo.OffsetTimeByPixels(t, minPix);
nc.SetValue(result);
result = nc.GetValue();
return result;
}
double OffsetTime
(AudacityProject &project,
double t, double offset, TimeUnit timeUnit, int snapToTime)
{
auto &viewInfo = ViewInfo::Get( project );
if (timeUnit == TIME_UNIT_SECONDS)
return t + offset; // snapping is currently ignored for non-pixel moves
if (snapToTime == SNAP_OFF)
return viewInfo.OffsetTimeByPixels(t, (int)offset);
return GridMove(project, t, (int)offset);
}
// Moving a cursor, and collapsed selection.
void MoveWhenAudioInactive
(AudacityProject &project, double seekStep, TimeUnit timeUnit)
{
auto &viewInfo = ViewInfo::Get( project );
auto trackPanel = project.GetTrackPanel();
auto &tracks = TrackList::Get( project );
auto ruler = project.GetRulerPanel();
// If TIME_UNIT_SECONDS, snap-to will be off.
int snapToTime = project.GetSnapTo();
const double t0 = viewInfo.selectedRegion.t0();
const double end = std::max(
tracks.GetEndTime(),
trackPanel->GetScreenEndTime());
// Move the cursor
// Already in cursor mode?
if( viewInfo.selectedRegion.isPoint() )
{
double newT = OffsetTime(project,
t0, seekStep, timeUnit, snapToTime);
// constrain.
newT = std::max(0.0, newT);
newT = std::min(newT, end);
// Move
viewInfo.selectedRegion.setT0(
newT,
false); // do not swap selection boundaries
viewInfo.selectedRegion.collapseToT0();
// Move the visual cursor, avoiding an unnecessary complete redraw
trackPanel->DrawOverlays(false);
ruler->DrawOverlays(false);
// This updates the selection shown on the selection bar, and the play
// region
project.TP_DisplaySelection();
} else
{
// Transition to cursor mode.
if( seekStep < 0 )
viewInfo.selectedRegion.collapseToT0();
else
viewInfo.selectedRegion.collapseToT1();
trackPanel->Refresh(false);
}
// Make sure NEW position is in view
trackPanel->ScrollIntoView(viewInfo.selectedRegion.t1());
return;
}
void SeekWhenAudioInactive
(AudacityProject &project, double seekStep, TimeUnit timeUnit,
SelectionOperation operation)
{
auto &viewInfo = ViewInfo::Get( project );
auto trackPanel = project.GetTrackPanel();
auto &tracks = TrackList::Get( project );
if( operation == CURSOR_MOVE )
{
MoveWhenAudioInactive( project, seekStep, timeUnit);
return;
}
int snapToTime = project.GetSnapTo();
const double t0 = viewInfo.selectedRegion.t0();
const double t1 = viewInfo.selectedRegion.t1();
const double end = std::max(
tracks.GetEndTime(),
trackPanel->GetScreenEndTime());
// Is it t0 or t1 moving?
bool bMoveT0 = (operation == SELECTION_CONTRACT && seekStep > 0) ||
(operation == SELECTION_EXTEND && seekStep < 0);
// newT is where we want to move to
double newT = OffsetTime( project,
bMoveT0 ? t0 : t1, seekStep, timeUnit, snapToTime);
// constrain to be in the track/screen limits.
newT = std::max( 0.0, newT );
newT = std::min( newT, end);
// optionally constrain to be a contraction, i.e. so t0/t1 do not cross over
if( operation == SELECTION_CONTRACT )
newT = bMoveT0 ? std::min( t1, newT ) : std::max( t0, newT );
// Actually move
if( bMoveT0 )
viewInfo.selectedRegion.setT0( newT );
else
viewInfo.selectedRegion.setT1( newT );
// Ensure it is visible, and refresh.
trackPanel->ScrollIntoView(newT);
trackPanel->Refresh(false);
}
// Handle small cursor and play head movements
void SeekLeftOrRight
(AudacityProject &project, double direction, SelectionOperation operation,
SeekInfo &info)
{
// PRL: What I found and preserved, strange though it be:
// During playback: jump depends on preferences and is independent of the
// zoom and does not vary if the key is held
// Else: jump depends on the zoom and gets bigger if the key is held
if( project.IsAudioActive() )
{
if( operation == CURSOR_MOVE )
SeekWhenAudioActive( info.mSeekShort * direction,
info.mLastSelectionAdjustment );
else if( operation == SELECTION_EXTEND )
SeekWhenAudioActive( info.mSeekLong * direction,
info.mLastSelectionAdjustment );
// Note: no action for CURSOR_CONTRACT
return;
}
// If the last adjustment was very recent, we are
// holding the key down and should move faster.
const wxLongLong curtime = ::wxGetUTCTimeMillis();
enum { MIN_INTERVAL = 50 };
const bool fast =
(curtime - info.mLastSelectionAdjustment < MIN_INTERVAL);
info.mLastSelectionAdjustment = curtime;
// How much faster should the cursor move if shift is down?
enum { LARGER_MULTIPLIER = 4 };
const double seekStep = (fast ? LARGER_MULTIPLIER : 1.0) * direction;
SeekWhenAudioInactive( project, seekStep, TIME_UNIT_PIXELS, operation);
}
// Move the cursor forward or backward, while paused or while playing.
void DoCursorMove(
AudacityProject &project, double seekStep,
wxLongLong &lastSelectionAdjustment)
{
if (project.IsAudioActive()) {
SeekWhenAudioActive(seekStep, lastSelectionAdjustment);
}
else
{
lastSelectionAdjustment = ::wxGetUTCTimeMillis();
MoveWhenAudioInactive(project, seekStep, TIME_UNIT_SECONDS);
}
project.ModifyState(false);
}
void DoBoundaryMove(AudacityProject &project, int step, SeekInfo &info)
{
auto &viewInfo = ViewInfo::Get( project );
auto trackPanel = project.GetTrackPanel();
auto &tracks = TrackList::Get( project );
// step is negative, then is moving left. step positive, moving right.
// Move the left/right selection boundary, to expand the selection
// If the last adjustment was very recent, we are
// holding the key down and should move faster.
wxLongLong curtime = ::wxGetUTCTimeMillis();
int pixels = step;
if( curtime - info.mLastSelectionAdjustment < 50 )
{
pixels *= 4;
}
info.mLastSelectionAdjustment = curtime;
// we used to have a parameter boundaryContract to say if expanding or
// contracting. it is no longer needed.
bool bMoveT0 = (step < 0 );// ^ boundaryContract ;
if( project.IsAudioActive() )
{
double indicator = gAudioIO->GetStreamTime();
if( bMoveT0 )
viewInfo.selectedRegion.setT0(indicator, false);
else
viewInfo.selectedRegion.setT1(indicator);
project.ModifyState(false);
trackPanel->Refresh(false);
return;
}
const double t0 = viewInfo.selectedRegion.t0();
const double t1 = viewInfo.selectedRegion.t1();
const double end = std::max(
tracks.GetEndTime(),
trackPanel->GetScreenEndTime());
double newT = viewInfo.OffsetTimeByPixels( bMoveT0 ? t0 : t1, pixels);
// constrain to be in the track/screen limits.
newT = std::max( 0.0, newT );
newT = std::min( newT, end);
// optionally constrain to be a contraction, i.e. so t0/t1 do not cross over
//if( boundaryContract )
// newT = bMoveT0 ? std::min( t1, newT ) : std::max( t0, newT );
// Actually move
if( bMoveT0 )
viewInfo.selectedRegion.setT0( newT );
else
viewInfo.selectedRegion.setT1( newT );
// Ensure it is visible, and refresh.
trackPanel->ScrollIntoView(newT);
trackPanel->Refresh(false);
project.ModifyState(false);
}
}
namespace SelectActions {
// exported helper functions
void SelectNone( AudacityProject &project )
{
auto &tracks = TrackList::Get( project );
for (auto t : tracks.Any())
t->SetSelected(false);
project.GetTrackPanel()->Refresh(false);
}
// Select the full time range, if no
// time range is selected.
void SelectAllIfNone( AudacityProject &project )
{
auto &viewInfo = ViewInfo::Get( project );
auto flags = GetMenuManager( project ).GetUpdateFlags( project );
if(!(flags & TracksSelectedFlag) ||
viewInfo.selectedRegion.isPoint())
DoSelectAllAudio( project );
}
void DoListSelection
(AudacityProject &project, Track *t, bool shift, bool ctrl, bool modifyState)
{
auto &tracks = TrackList::Get( project );
auto trackPanel = project.GetTrackPanel();
auto &selectionState = project.GetSelectionState();
auto &viewInfo = ViewInfo::Get( project );
auto isSyncLocked = project.IsSyncLocked();
selectionState.HandleListSelection(
tracks, viewInfo, *t,
shift, ctrl, isSyncLocked );
if (! ctrl )
trackPanel->SetFocusedTrack(t);
project.Refresh(false);
if (modifyState)
project.ModifyState(true);
}
void DoSelectAll(AudacityProject &project)
{
DoSelectTimeAndTracks( project, true, true );
}
void DoSelectAllAudio(AudacityProject &project)
{
DoSelectTimeAndAudioTracks( project, true, true );
}
// This function selects all tracks if no tracks selected,
// and all time if no time selected.
// There is an argument for making it just count wave tracks,
// However you could then not select a label and cut it,
// without this function selecting all tracks.
void DoSelectSomething(AudacityProject &project)
{
auto &tracks = TrackList::Get( project );
auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;
bool bTime = selectedRegion.isPoint();
bool bTracks = tracks.Selected().empty();
if( bTime || bTracks )
DoSelectTimeAndTracks( project, bTime, bTracks );
}
// Menu handler functions
struct Handler
: CommandHandlerObject // MUST be the first base class!
, ClientData::Base
, PrefsListener
{
void OnSelectAll(const CommandContext &context)
{
DoSelectAll( context.project );
}
void OnSelectNone(const CommandContext &context)
{
auto &project = context.project;
auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;
selectedRegion.collapseToT0();
SelectNone( project );
project.ModifyState(false);
}
void OnSelectAllTracks(const CommandContext &context)
{
auto &project = context.project;
DoSelectTimeAndTracks( project, false, true );
}
void OnSelectSyncLockSel(const CommandContext &context)
{
auto &project = context.project;
auto &tracks = TrackList::Get( project );
auto trackPanel = project.GetTrackPanel();
bool selected = false;
for (auto t : tracks.Any()
+ &Track::IsSyncLockSelected - &Track::IsSelected) {
t->SetSelected(true);
selected = true;
}
if (selected)
project.ModifyState(false);
trackPanel->Refresh(false);
}
//this pops up a dialog which allows the left selection to be set.
//If playing/recording is happening, it sets the left selection at
//the current play position.
void OnSetLeftSelection(const CommandContext &context)
{
auto &project = context.project;
auto token = project.GetAudioIOToken();
auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;
auto trackPanel = project.GetTrackPanel();
bool bSelChanged = false;
if ((token > 0) && gAudioIO->IsStreamActive(token))
{
double indicator = gAudioIO->GetStreamTime();
selectedRegion.setT0(indicator, false);
bSelChanged = true;
}
else
{
auto fmt = project.GetSelectionFormat();
auto rate = project.GetRate();
TimeDialog dlg(&project, _("Set Left Selection Boundary"),
fmt, rate, selectedRegion.t0(), _("Position"));
if (wxID_OK == dlg.ShowModal())
{
//Get the value from the dialog
selectedRegion.setT0(
std::max(0.0, dlg.GetTimeValue()), false);
bSelChanged = true;
}
}
if (bSelChanged)
{
project.ModifyState(false);
trackPanel->Refresh(false);
}
}
void OnSetRightSelection(const CommandContext &context)
{
auto &project = context.project;
auto token = project.GetAudioIOToken();
auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;
auto trackPanel = project.GetTrackPanel();
bool bSelChanged = false;
if ((token > 0) && gAudioIO->IsStreamActive(token))
{
double indicator = gAudioIO->GetStreamTime();
selectedRegion.setT1(indicator, false);
bSelChanged = true;
}
else
{
auto fmt = project.GetSelectionFormat();
auto rate = project.GetRate();
TimeDialog dlg(&project, _("Set Right Selection Boundary"),
fmt, rate, selectedRegion.t1(), _("Position"));
if (wxID_OK == dlg.ShowModal())
{
//Get the value from the dialog
selectedRegion.setT1(
std::max(0.0, dlg.GetTimeValue()), false);
bSelChanged = true;
}
}
if (bSelChanged)
{
project.ModifyState(false);
trackPanel->Refresh(false);
}
}
void OnSelectStartCursor(const CommandContext &context)
{
auto &project = context.project;
auto &tracks = TrackList::Get( project );
auto trackPanel = project.GetTrackPanel();
auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;
double kWayOverToRight = std::numeric_limits<double>::max();
auto range = tracks.Selected();
if ( ! range )
return;
double minOffset = range.min( &Track::GetStartTime );
if( minOffset >=
(kWayOverToRight * (1 - std::numeric_limits<double>::epsilon()) ))
return;
selectedRegion.setT0(minOffset);
project.ModifyState(false);
trackPanel->Refresh(false);
}
void OnSelectCursorEnd(const CommandContext &context)
{
auto &project = context.project;
auto &tracks = TrackList::Get( project );
auto trackPanel = project.GetTrackPanel();
auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;
double kWayOverToLeft = std::numeric_limits<double>::lowest();
auto range = tracks.Selected();
if ( ! range )
return;
double maxEndOffset = range.max( &Track::GetEndTime );
if( maxEndOffset <=
(kWayOverToLeft * (1 - std::numeric_limits<double>::epsilon()) ))
return;
selectedRegion.setT1(maxEndOffset);
project.ModifyState(false);
trackPanel->Refresh(false);
}
void OnSelectTrackStartToEnd(const CommandContext &context)
{
auto &project = context.project;
auto &viewInfo = ViewInfo::Get( project );
auto &tracks = TrackList::Get( project );
auto trackPanel = project.GetTrackPanel();
auto range = tracks.Selected();
double maxEndOffset = range.max( &Track::GetEndTime );
double minOffset = range.min( &Track::GetStartTime );
if( maxEndOffset < minOffset)
return;
viewInfo.selectedRegion.setTimes( minOffset, maxEndOffset );
project.ModifyState(false);
trackPanel->Refresh(false);
}
// Handler state:
SelectedRegion mRegionSave{};
void OnSelectionSave(const CommandContext &context)
{
auto &project = context.project;
auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;
mRegionSave = selectedRegion;
}
void OnSelectionRestore(const CommandContext &context)
{
auto &project = context.project;
auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;
auto trackPanel = project.GetTrackPanel();
if ((mRegionSave.t0() == 0.0) &&
(mRegionSave.t1() == 0.0))
return;
selectedRegion = mRegionSave;
project.ModifyState(false);
trackPanel->Refresh(false);
}
#ifdef EXPERIMENTAL_SPECTRAL_EDITING
// Handler state:
double mLastF0{ SelectedRegion::UndefinedFrequency };
double mLastF1{ SelectedRegion::UndefinedFrequency };
void OnToggleSpectralSelection(const CommandContext &context)
{
auto &project = context.project;
auto trackPanel = project.GetTrackPanel();
auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;
const double f0 = selectedRegion.f0();
const double f1 = selectedRegion.f1();
const bool haveSpectralSelection =
!(f0 == SelectedRegion::UndefinedFrequency &&
f1 == SelectedRegion::UndefinedFrequency);
if (haveSpectralSelection)
{
mLastF0 = f0;
mLastF1 = f1;
selectedRegion.setFrequencies
(SelectedRegion::UndefinedFrequency, SelectedRegion::UndefinedFrequency);
}
else
selectedRegion.setFrequencies(mLastF0, mLastF1);
trackPanel->Refresh(false);
project.ModifyState(false);
}
void OnNextHigherPeakFrequency(const CommandContext &context)
{
auto &project = context.project;
DoNextPeakFrequency(project, true);
}
void OnNextLowerPeakFrequency(const CommandContext &context)
{
auto &project = context.project;
DoNextPeakFrequency(project, false);
}
#endif
// Handler state:
bool mCursorPositionHasBeenStored{ false };
double mCursorPositionStored{ 0.0 };
void OnSelectCursorStoredCursor(const CommandContext &context)
{
auto &project = context.project;
auto trackPanel = project.GetTrackPanel();
auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;
auto isAudioActive = project.IsAudioActive();
if (mCursorPositionHasBeenStored) {
double cursorPositionCurrent = isAudioActive
? gAudioIO->GetStreamTime()
: selectedRegion.t0();
selectedRegion.setTimes(
std::min(cursorPositionCurrent, mCursorPositionStored),
std::max(cursorPositionCurrent, mCursorPositionStored));
project.ModifyState(false);
trackPanel->Refresh(false);
}
}
void OnCursorPositionStore(const CommandContext &context)
{
auto &project = context.project;
auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;
auto isAudioActive = project.IsAudioActive();
mCursorPositionStored =
isAudioActive ? gAudioIO->GetStreamTime() : selectedRegion.t0();
mCursorPositionHasBeenStored = true;
}
void OnZeroCrossing(const CommandContext &context)
{
auto &project = context.project;
auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;
auto trackPanel = project.GetTrackPanel();
const double t0 = NearestZeroCrossing(project, selectedRegion.t0());
if (selectedRegion.isPoint())
selectedRegion.setTimes(t0, t0);
else {
const double t1 = NearestZeroCrossing(project, selectedRegion.t1());
// Empty selection is generally not much use, so do not make it if empty.
if( fabs( t1 - t0 ) * project.GetRate() > 1.5 )
selectedRegion.setTimes(t0, t1);
}
project.ModifyState(false);
trackPanel->Refresh(false);
}
void OnSnapToOff(const CommandContext &context)
{
auto &project = context.project;
project.SetSnapTo(SNAP_OFF);
}
void OnSnapToNearest(const CommandContext &context)
{
auto &project = context.project;
project.SetSnapTo(SNAP_NEAREST);
}
void OnSnapToPrior(const CommandContext &context)
{
auto &project = context.project;
project.SetSnapTo(SNAP_PRIOR);
}
void OnSelToStart(const CommandContext &context)
{
auto &project = context.project;
project.Rewind(true);
project.ModifyState(false);
}
void OnSelToEnd(const CommandContext &context)
{
auto &project = context.project;
project.SkipEnd(true);
project.ModifyState(false);
}
// Handler state:
SeekInfo mSeekInfo;
void OnSelExtendLeft(const CommandContext &context)
{
if( !OnlyHandleKeyUp( context ) )
SeekLeftOrRight( context.project, DIRECTION_LEFT, SELECTION_EXTEND,
mSeekInfo );
}
void OnSelExtendRight(const CommandContext &context)
{
if( !OnlyHandleKeyUp( context ) )
SeekLeftOrRight( context.project, DIRECTION_RIGHT, SELECTION_EXTEND,
mSeekInfo );
}
void OnSelSetExtendLeft(const CommandContext &context)
{
DoBoundaryMove( context.project, DIRECTION_LEFT, mSeekInfo);
}
void OnSelSetExtendRight(const CommandContext &context)
{
DoBoundaryMove( context.project, DIRECTION_RIGHT, mSeekInfo);
}
void OnSelContractLeft(const CommandContext &context)
{
if( !OnlyHandleKeyUp( context ) )
SeekLeftOrRight( context.project, DIRECTION_RIGHT, SELECTION_CONTRACT,
mSeekInfo );
}
void OnSelContractRight(const CommandContext &context)
{
if( !OnlyHandleKeyUp( context ) )
SeekLeftOrRight( context.project, DIRECTION_LEFT, SELECTION_CONTRACT,
mSeekInfo );
}
void OnCursorSelStart(const CommandContext &context)
{
auto &project = context.project;
auto trackPanel = project.GetTrackPanel();
auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;
selectedRegion.collapseToT0();
project.ModifyState(false);
trackPanel->ScrollIntoView(selectedRegion.t0());
trackPanel->Refresh(false);
}
void OnCursorSelEnd(const CommandContext &context)
{
auto &project = context.project;
auto trackPanel = project.GetTrackPanel();
auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;
selectedRegion.collapseToT1();
project.ModifyState(false);
trackPanel->ScrollIntoView(selectedRegion.t1());
trackPanel->Refresh(false);
}
void OnCursorTrackStart(const CommandContext &context)
{
auto &project = context.project;
auto &tracks = TrackList::Get( project );
auto trackPanel = project.GetTrackPanel();
auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;
double kWayOverToRight = std::numeric_limits<double>::max();
auto trackRange = tracks.Selected();
if (trackRange.empty())
// This should have been prevented by command manager
return;
// Range is surely nonempty now
auto minOffset = std::max( 0.0, trackRange.min( &Track::GetOffset ) );
if( minOffset >=
(kWayOverToRight * (1 - std::numeric_limits<double>::epsilon()) ))
return;
selectedRegion.setTimes(minOffset, minOffset);
project.ModifyState(false);
trackPanel->ScrollIntoView(selectedRegion.t0());
trackPanel->Refresh(false);
}
void OnCursorTrackEnd(const CommandContext &context)
{
auto &project = context.project;
auto &tracks = TrackList::Get( project );
auto trackPanel = project.GetTrackPanel();
auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;
double kWayOverToLeft = std::numeric_limits<double>::lowest();
auto trackRange = tracks.Selected();
if (trackRange.empty())
// This should have been prevented by command manager
return;
// Range is surely nonempty now
auto maxEndOffset = trackRange.max( &Track::GetEndTime );
if( maxEndOffset <
(kWayOverToLeft * (1 - std::numeric_limits<double>::epsilon()) ))
return;
selectedRegion.setTimes(maxEndOffset, maxEndOffset);
project.ModifyState(false);
trackPanel->ScrollIntoView(selectedRegion.t1());
trackPanel->Refresh(false);
}
void OnSkipStart(const CommandContext &context)
{
auto &project = context.project;
wxCommandEvent evt;
auto controlToolBar = project.GetControlToolBar();
controlToolBar->OnRewind(evt);
project.ModifyState(false);
}
void OnSkipEnd(const CommandContext &context)
{
auto &project = context.project;
wxCommandEvent evt;
auto controlToolBar = project.GetControlToolBar();
controlToolBar->OnFF(evt);
project.ModifyState(false);
}
void OnCursorLeft(const CommandContext &context)
{
if( !OnlyHandleKeyUp( context ) )
SeekLeftOrRight( context.project, DIRECTION_LEFT, CURSOR_MOVE,
mSeekInfo );
}
void OnCursorRight(const CommandContext &context)
{
if( !OnlyHandleKeyUp( context ) )
SeekLeftOrRight( context.project, DIRECTION_RIGHT, CURSOR_MOVE,
mSeekInfo );
}
void OnCursorShortJumpLeft(const CommandContext &context)
{
DoCursorMove( context.project,
-mSeekInfo.mSeekShort, mSeekInfo.mLastSelectionAdjustment );
}
void OnCursorShortJumpRight(const CommandContext &context)
{
DoCursorMove( context.project,
mSeekInfo.mSeekShort, mSeekInfo.mLastSelectionAdjustment );
}
void OnCursorLongJumpLeft(const CommandContext &context)
{
DoCursorMove( context.project,
-mSeekInfo.mSeekLong, mSeekInfo.mLastSelectionAdjustment );
}
void OnCursorLongJumpRight(const CommandContext &context)
{
DoCursorMove( context.project,
mSeekInfo.mSeekLong, mSeekInfo.mLastSelectionAdjustment );
}
void OnSeekLeftShort(const CommandContext &context)
{
auto &project = context.project;
SeekLeftOrRight( project, DIRECTION_LEFT, CURSOR_MOVE, mSeekInfo );
}
void OnSeekRightShort(const CommandContext &context)
{
auto &project = context.project;
SeekLeftOrRight( project, DIRECTION_RIGHT, CURSOR_MOVE, mSeekInfo );
}
void OnSeekLeftLong(const CommandContext &context)
{
auto &project = context.project;
SeekLeftOrRight( project, DIRECTION_LEFT, SELECTION_EXTEND, mSeekInfo );
}
void OnSeekRightLong(const CommandContext &context)
{
auto &project = context.project;
SeekLeftOrRight( project, DIRECTION_RIGHT, SELECTION_EXTEND, mSeekInfo );
}
#if 1
// Legacy functions, not used as of version 2.3.0
void OnSelectAllTime(const CommandContext &context)
{
auto &project = context.project;
DoSelectTimeAndTracks( project, true, false );
}
#endif
void UpdatePrefs() override
{
gPrefs->Read(wxT("/AudioIO/SeekShortPeriod"), &mSeekInfo.mSeekShort, 1.0);
gPrefs->Read(wxT("/AudioIO/SeekLongPeriod"), &mSeekInfo.mSeekLong, 15.0);
}
Handler()
{
UpdatePrefs();
}
}; // struct Handler
} // namespace
// Handler is stateful. Needs a factory registered with
// AudacityProject.
static const AudacityProject::AttachedObjects::RegisteredFactory key{
[](AudacityProject&) {
return std::make_unique< SelectActions::Handler >(); } };
static CommandHandlerObject &findCommandHandler(AudacityProject &project) {
return project.AttachedObjects::Get< SelectActions::Handler >( key );
};
// Menu definitions
#define FN(X) findCommandHandler, \
static_cast<CommandFunctorPointer>(& SelectActions::Handler :: X)
#define XXO(X) _(X), wxString{X}.Contains("...")
MenuTable::BaseItemPtr ClipSelectMenu( AudacityProject& );
MenuTable::BaseItemPtr SelectMenu( AudacityProject& )
{
using namespace MenuTable;
using Options = CommandManager::Options;
/* i18n-hint: (verb) It's an item on a menu. */
return Menu( _("&Select"),
Command( wxT("SelectAll"), XXO("&All"), FN(OnSelectAll),
TracksExistFlag,
Options{ wxT("Ctrl+A"), _("Select All") } ),
Command( wxT("SelectNone"), XXO("&None"), FN(OnSelectNone),
TracksExistFlag,
Options{ wxT("Ctrl+Shift+A"), _("Select None") } ),
//////////////////////////////////////////////////////////////////////////
Menu( _("&Tracks"),
Command( wxT("SelAllTracks"), XXO("In All &Tracks"),
FN(OnSelectAllTracks),
TracksExistFlag,
wxT("Ctrl+Shift+K") )
#ifdef EXPERIMENTAL_SYNC_LOCK
,
Command( wxT("SelSyncLockTracks"), XXO("In All &Sync-Locked Tracks"),
FN(OnSelectSyncLockSel),
TracksSelectedFlag | IsSyncLockedFlag,
Options{ wxT("Ctrl+Shift+Y"), _("Select Sync-Locked") } )
#endif
),
//////////////////////////////////////////////////////////////////////////
Menu( _("R&egion"),
Command( wxT("SetLeftSelection"), XXO("&Left at Playback Position"),
FN(OnSetLeftSelection), TracksExistFlag,
Options{ wxT("["), _("Set Selection Left at Play Position") } ),
Command( wxT("SetRightSelection"), XXO("&Right at Playback Position"),
FN(OnSetRightSelection), TracksExistFlag,
Options{ wxT("]"), _("Set Selection Right at Play Position") } ),
Command( wxT("SelTrackStartToCursor"), XXO("Track &Start to Cursor"),
FN(OnSelectStartCursor), AlwaysEnabledFlag,
Options{ wxT("Shift+J"), _("Select Track Start to Cursor") } ),
Command( wxT("SelCursorToTrackEnd"), XXO("Cursor to Track &End"),
FN(OnSelectCursorEnd), AlwaysEnabledFlag,
Options{ wxT("Shift+K"), _("Select Cursor to Track End") } ),
Command( wxT("SelTrackStartToEnd"), XXO("Track Start to En&d"),
FN(OnSelectTrackStartToEnd), AlwaysEnabledFlag,
Options{}.LongName( _("Select Track Start to End") ) ),
Separator(),
// GA: Audacity had 'Store Re&gion' here previously. There is no
// one-step way to restore the 'Saved Cursor Position' in Select Menu,
// so arguably using the word 'Selection' to do duty for both saving
// the region or the cursor is better. But it does not belong in a
// 'Region' submenu.
Command( wxT("SelSave"), XXO("S&tore Selection"), FN(OnSelectionSave),
WaveTracksSelectedFlag ),
// Audacity had 'Retrieve Regio&n' here previously.
Command( wxT("SelRestore"), XXO("Retrieve Selectio&n"),
FN(OnSelectionRestore), TracksExistFlag )
),
//////////////////////////////////////////////////////////////////////////
#ifdef EXPERIMENTAL_SPECTRAL_EDITING
Menu( _("S&pectral"),
Command( wxT("ToggleSpectralSelection"),
XXO("To&ggle Spectral Selection"), FN(OnToggleSpectralSelection),
TracksExistFlag, wxT("Q") ),
Command( wxT("NextHigherPeakFrequency"),
XXO("Next &Higher Peak Frequency"), FN(OnNextHigherPeakFrequency),
TracksExistFlag ),
Command( wxT("NextLowerPeakFrequency"),
XXO("Next &Lower Peak Frequency"), FN(OnNextLowerPeakFrequency),
TracksExistFlag )
),
#endif
//////////////////////////////////////////////////////////////////////////
ClipSelectMenu,
//////////////////////////////////////////////////////////////////////////
Separator(),
Command( wxT("SelCursorStoredCursor"),
XXO("Cursor to Stored &Cursor Position"),
FN(OnSelectCursorStoredCursor), TracksExistFlag,
Options{}.LongName( _("Select Cursor to Stored") ) ),
Command( wxT("StoreCursorPosition"), XXO("Store Cursor Pos&ition"),
FN(OnCursorPositionStore),
WaveTracksExistFlag ),
// Save cursor position is used in some selections.
// Maybe there should be a restore for it?
Separator(),
Command( wxT("ZeroCross"), XXO("At &Zero Crossings"),
FN(OnZeroCrossing), TracksSelectedFlag,
Options{ wxT("Z"), _("Select Zero Crossing") } )
);
}
MenuTable::BaseItemPtr ExtraSelectionMenu( AudacityProject & )
{
using namespace MenuTable;
return Menu( _("&Selection"),
Command( wxT("SnapToOff"), XXO("Snap-To &Off"), FN(OnSnapToOff),
AlwaysEnabledFlag ),
Command( wxT("SnapToNearest"), XXO("Snap-To &Nearest"),
FN(OnSnapToNearest), AlwaysEnabledFlag ),
Command( wxT("SnapToPrior"), XXO("Snap-To &Prior"), FN(OnSnapToPrior),
AlwaysEnabledFlag ),
Command( wxT("SelStart"), XXO("Selection to &Start"), FN(OnSelToStart),
AlwaysEnabledFlag, wxT("Shift+Home") ),
Command( wxT("SelEnd"), XXO("Selection to En&d"), FN(OnSelToEnd),
AlwaysEnabledFlag, wxT("Shift+End") ),
Command( wxT("SelExtLeft"), XXO("Selection Extend &Left"),
FN(OnSelExtendLeft),
TracksExistFlag | TrackPanelHasFocus,
wxT("Shift+Left\twantKeyup\tallowDup") ),
Command( wxT("SelExtRight"), XXO("Selection Extend &Right"),
FN(OnSelExtendRight),
TracksExistFlag | TrackPanelHasFocus,
wxT("Shift+Right\twantKeyup\tallowDup") ),
Command( wxT("SelSetExtLeft"), XXO("Set (or Extend) Le&ft Selection"),
FN(OnSelSetExtendLeft),
TracksExistFlag | TrackPanelHasFocus ),
Command( wxT("SelSetExtRight"), XXO("Set (or Extend) Rig&ht Selection"),
FN(OnSelSetExtendRight),
TracksExistFlag | TrackPanelHasFocus ),
Command( wxT("SelCntrLeft"), XXO("Selection Contract L&eft"),
FN(OnSelContractLeft),
TracksExistFlag | TrackPanelHasFocus,
wxT("Ctrl+Shift+Right\twantKeyup") ),
Command( wxT("SelCntrRight"), XXO("Selection Contract R&ight"),
FN(OnSelContractRight),
TracksExistFlag | TrackPanelHasFocus,
wxT("Ctrl+Shift+Left\twantKeyup") )
);
}
MenuTable::BaseItemPtr ClipCursorItems( AudacityProject & );
MenuTable::BaseItemPtr CursorMenu( AudacityProject & )
{
using namespace MenuTable;
using Options = CommandManager::Options;
constexpr auto CanStopFlags = AudioIONotBusyFlag | CanStopAudioStreamFlag;
// JKC: ANSWER-ME: How is 'cursor to' different to 'Skip To' and how is it
// useful?
// GA: 'Skip to' moves the viewpoint to center of the track and preserves the
// selection. 'Cursor to' does neither. 'Center at' might describe it better
// than 'Skip'.
return Menu( _("&Cursor to"),
Command( wxT("CursSelStart"), XXO("Selection Star&t"),
FN(OnCursorSelStart),
TimeSelectedFlag,
Options{}.LongName( _("Cursor to Selection Start") ) ),
Command( wxT("CursSelEnd"), XXO("Selection En&d"),
FN(OnCursorSelEnd),
TimeSelectedFlag,
Options{}.LongName( _("Cursor to Selection End") ) ),
Command( wxT("CursTrackStart"), XXO("Track &Start"),
FN(OnCursorTrackStart),
TracksSelectedFlag,
Options{ wxT("J"), _("Cursor to Track Start") } ),
Command( wxT("CursTrackEnd"), XXO("Track &End"),
FN(OnCursorTrackEnd),
TracksSelectedFlag,
Options{ wxT("K"), _("Cursor to Track End") } ),
ClipCursorItems,
Command( wxT("CursProjectStart"), XXO("&Project Start"),
FN(OnSkipStart),
CanStopFlags,
Options{ wxT("Home"), _("Cursor to Project Start") } ),
Command( wxT("CursProjectEnd"), XXO("Project E&nd"), FN(OnSkipEnd),
CanStopFlags,
Options{ wxT("End"), _("Cursor to Project End") } )
);
}
MenuTable::BaseItemPtr ExtraClipCursorItems( AudacityProject & );
MenuTable::BaseItemPtr ExtraCursorMenu( AudacityProject & )
{
using namespace MenuTable;
return Menu( _("&Cursor"),
Command( wxT("CursorLeft"), XXO("Cursor &Left"), FN(OnCursorLeft),
TracksExistFlag | TrackPanelHasFocus,
wxT("Left\twantKeyup\tallowDup") ),
Command( wxT("CursorRight"), XXO("Cursor &Right"), FN(OnCursorRight),
TracksExistFlag | TrackPanelHasFocus,
wxT("Right\twantKeyup\tallowDup") ),
Command( wxT("CursorShortJumpLeft"), XXO("Cursor Sh&ort Jump Left"),
FN(OnCursorShortJumpLeft),
TracksExistFlag | TrackPanelHasFocus, wxT(",") ),
Command( wxT("CursorShortJumpRight"), XXO("Cursor Shor&t Jump Right"),
FN(OnCursorShortJumpRight),
TracksExistFlag | TrackPanelHasFocus, wxT(".") ),
Command( wxT("CursorLongJumpLeft"), XXO("Cursor Long J&ump Left"),
FN(OnCursorLongJumpLeft),
TracksExistFlag | TrackPanelHasFocus, wxT("Shift+,") ),
Command( wxT("CursorLongJumpRight"), XXO("Cursor Long Ju&mp Right"),
FN(OnCursorLongJumpRight),
TracksExistFlag | TrackPanelHasFocus, wxT("Shift+.") ),
ExtraClipCursorItems
);
}
MenuTable::BaseItemPtr ExtraSeekMenu( AudacityProject & )
{
using namespace MenuTable;
return Menu( _("See&k"),
Command( wxT("SeekLeftShort"), XXO("Short Seek &Left During Playback"),
FN(OnSeekLeftShort), AudioIOBusyFlag, wxT("Left\tallowDup") ),
Command( wxT("SeekRightShort"),
XXO("Short Seek &Right During Playback"), FN(OnSeekRightShort),
AudioIOBusyFlag, wxT("Right\tallowDup") ),
Command( wxT("SeekLeftLong"), XXO("Long Seek Le&ft During Playback"),
FN(OnSeekLeftLong), AudioIOBusyFlag, wxT("Shift+Left\tallowDup") ),
Command( wxT("SeekRightLong"), XXO("Long Seek Rig&ht During Playback"),
FN(OnSeekRightLong), AudioIOBusyFlag, wxT("Shift+Right\tallowDup") )
);
}
#undef XXO
#undef FN