mirror of
https://github.com/cookiengineer/audacity
synced 2025-11-21 16:37:12 +01:00
Problem: Currently calling Track::EnsureVisible() also sets the track as focus. In Audacity 2.3.3 the timing of the code to set the focus was changed. Rather than a direct call, an event is queued, and then the focus is set. This has changed the timing of the focus event which is sent with respect to other focus and name change events. In particular in the case of toggling the selectness of the focused track, this moved the focus event to be after the name change event. These changes only had an effect on NVDA - Jaws and Narrator were unaffected. The introduction of this bug has highlighted an existing problem. 1. There are a small number of existing cases where a track needs to be visible, but where it is already the focus, and so setting the focus is unnecessary. For example, pressing Enter to toggle whether a track is selected. 2. Some of the Audacity code which calls EnsureVisible() is written with the assumption that EnsureVisible() doesn't set the focus, and so there are unnecessary focus events. Whilst other code which calls EnsureVisible() assumes that it also sets the focus. Confusion. The Fix: Remove the setting of focus from within Track::EnsureVisible(), and so remove the unnecessary focus events. Calls to set the focus were added before calls to EnsureVisible where the code was relying on EnsureVisible to set the focus. In TrackPanel::ProcessUIHandleResult, and TrackPanel::OnMouseEvent, I wasn't sure if the focus needed to be set, so called it anyway to ensure that the behaviour did not change. So I would like to remove the setting of focus from within Track::EnsureVisible(), and add explicit calls to set the focus where necessary. I think this would make the code clearer, remove unnecessary calls to set the focus, and make it easier to keep NVDA happy.
1213 lines
37 KiB
C++
1213 lines
37 KiB
C++
#include "../Audacity.h" // for USE_* macros
|
|
#include "../AdornedRulerPanel.h"
|
|
#include "../Clipboard.h"
|
|
#include "../CommonCommandFlags.h"
|
|
#include "../LabelTrack.h"
|
|
#include "../Menus.h"
|
|
#include "../NoteTrack.h"
|
|
#include "../Prefs.h"
|
|
#include "../Project.h"
|
|
#include "../ProjectHistory.h"
|
|
#include "../ProjectSettings.h"
|
|
#include "../ProjectWindow.h"
|
|
#include "../SelectUtilities.h"
|
|
#include "../TimeTrack.h"
|
|
#include "../TrackPanel.h"
|
|
#include "../TrackPanelAx.h"
|
|
#include "../UndoManager.h"
|
|
#include "../ViewInfo.h"
|
|
#include "../WaveTrack.h"
|
|
#include "../commands/CommandContext.h"
|
|
#include "../commands/CommandManager.h"
|
|
#include "../commands/ScreenshotCommand.h"
|
|
#include "../export/Export.h"
|
|
#include "../prefs/PrefsDialog.h"
|
|
#include "../prefs/SpectrogramSettings.h"
|
|
#include "../prefs/WaveformSettings.h"
|
|
#include "../tracks/labeltrack/ui/LabelTrackView.h"
|
|
#include "../widgets/AudacityMessageBox.h"
|
|
|
|
// private helper classes and functions
|
|
namespace {
|
|
void FinishCopy
|
|
(const Track *n, const Track::Holder &dest, TrackList &list)
|
|
{
|
|
Track::FinishCopy( n, dest.get() );
|
|
if (dest)
|
|
list.Add( dest );
|
|
}
|
|
|
|
// Handle text paste (into active label), if any. Return true if did paste.
|
|
// (This was formerly the first part of overly-long OnPaste.)
|
|
bool DoPasteText(AudacityProject &project)
|
|
{
|
|
auto &tracks = TrackList::Get( project );
|
|
auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;
|
|
auto &window = ProjectWindow::Get( project );
|
|
|
|
for (auto pLabelTrack : tracks.Any<LabelTrack>())
|
|
{
|
|
// Does this track have an active label?
|
|
if (LabelTrackView::Get( *pLabelTrack ).HasSelection( project )) {
|
|
|
|
// Yes, so try pasting into it
|
|
auto &view = LabelTrackView::Get( *pLabelTrack );
|
|
if (view.PasteSelectedText( project, selectedRegion.t0(),
|
|
selectedRegion.t1() ))
|
|
{
|
|
ProjectHistory::Get( project )
|
|
.PushState(_("Pasted text from the clipboard"), _("Paste"));
|
|
|
|
// Make sure caret is in view
|
|
int x;
|
|
if (view.CalcCursorX( project, &x )) {
|
|
window.ScrollIntoView(x);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Return true if nothing selected, regardless of paste result.
|
|
// If nothing was selected, create and paste into NEW tracks.
|
|
// (This was formerly the second part of overly-long OnPaste.)
|
|
bool DoPasteNothingSelected(AudacityProject &project)
|
|
{
|
|
auto &tracks = TrackList::Get( project );
|
|
auto &trackFactory = TrackFactory::Get( project );
|
|
auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;
|
|
auto &window = ProjectWindow::Get( project );
|
|
|
|
// First check whether anything's selected.
|
|
if (tracks.Selected())
|
|
return false;
|
|
else
|
|
{
|
|
const auto &clipboard = Clipboard::Get();
|
|
auto clipTrackRange = clipboard.GetTracks().Any< const Track >();
|
|
if (clipTrackRange.empty())
|
|
return true; // nothing to paste
|
|
|
|
Track* pFirstNewTrack = NULL;
|
|
for (auto pClip : clipTrackRange) {
|
|
Maybe<WaveTrack::Locker> locker;
|
|
|
|
Track::Holder uNewTrack;
|
|
Track *pNewTrack;
|
|
pClip->TypeSwitch(
|
|
[&](const WaveTrack *wc) {
|
|
if ((clipboard.Project() != &project))
|
|
// Cause duplication of block files on disk, when copy is
|
|
// between projects
|
|
locker.create(wc);
|
|
uNewTrack = trackFactory.NewWaveTrack(
|
|
wc->GetSampleFormat(), wc->GetRate()),
|
|
pNewTrack = uNewTrack.get();
|
|
},
|
|
#ifdef USE_MIDI
|
|
[&](const NoteTrack *) {
|
|
uNewTrack = trackFactory.NewNoteTrack(),
|
|
pNewTrack = uNewTrack.get();
|
|
},
|
|
#endif
|
|
[&](const LabelTrack *) {
|
|
uNewTrack = trackFactory.NewLabelTrack(),
|
|
pNewTrack = uNewTrack.get();
|
|
},
|
|
[&](const TimeTrack *) {
|
|
// Maintain uniqueness of the time track!
|
|
pNewTrack = *tracks.Any<TimeTrack>().begin();
|
|
if (!pNewTrack)
|
|
uNewTrack = trackFactory.NewTimeTrack(),
|
|
pNewTrack = uNewTrack.get();
|
|
}
|
|
);
|
|
|
|
wxASSERT(pClip);
|
|
|
|
pNewTrack->Paste(0.0, pClip);
|
|
|
|
if (!pFirstNewTrack)
|
|
pFirstNewTrack = pNewTrack;
|
|
|
|
pNewTrack->SetSelected(true);
|
|
if (uNewTrack)
|
|
FinishCopy(pClip, uNewTrack, tracks);
|
|
else
|
|
Track::FinishCopy(pClip, pNewTrack);
|
|
}
|
|
|
|
// Select some pasted samples, which is probably impossible to get right
|
|
// with various project and track sample rates.
|
|
// So do it at the sample rate of the project
|
|
AudacityProject *p = GetActiveProject();
|
|
double projRate = ProjectSettings::Get( *p ).GetRate();
|
|
double quantT0 = QUANTIZED_TIME(clipboard.T0(), projRate);
|
|
double quantT1 = QUANTIZED_TIME(clipboard.T1(), projRate);
|
|
selectedRegion.setTimes(
|
|
0.0, // anywhere else and this should be
|
|
// half a sample earlier
|
|
quantT1 - quantT0);
|
|
|
|
ProjectHistory::Get( project )
|
|
.PushState(_("Pasted from the clipboard"), _("Paste"));
|
|
|
|
if (pFirstNewTrack) {
|
|
TrackFocus::Get(project).Set(pFirstNewTrack);
|
|
pFirstNewTrack->EnsureVisible();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
namespace EditActions {
|
|
|
|
// Menu handler functions
|
|
|
|
struct Handler : CommandHandlerObject {
|
|
|
|
void OnUndo(const CommandContext &context)
|
|
{
|
|
auto &project = context.project;
|
|
auto &tracks = TrackList::Get( project );
|
|
auto &trackPanel = TrackPanel::Get( project );
|
|
auto &undoManager = UndoManager::Get( project );
|
|
auto &window = ProjectWindow::Get( project );
|
|
|
|
if (!ProjectHistory::Get( project ).UndoAvailable()) {
|
|
AudacityMessageBox(_("Nothing to undo"));
|
|
return;
|
|
}
|
|
|
|
// can't undo while dragging
|
|
if (trackPanel.IsMouseCaptured()) {
|
|
return;
|
|
}
|
|
|
|
undoManager.Undo(
|
|
[&]( const UndoState &state ){
|
|
ProjectHistory::Get( project ).PopState( state ); } );
|
|
|
|
auto t = *tracks.Selected().begin();
|
|
if (!t)
|
|
t = *tracks.Any().begin();
|
|
if (t) {
|
|
TrackFocus::Get(project).Set(t);
|
|
t->EnsureVisible();
|
|
}
|
|
}
|
|
|
|
void OnRedo(const CommandContext &context)
|
|
{
|
|
auto &project = context.project;
|
|
auto &tracks = TrackList::Get( project );
|
|
auto &trackPanel = TrackPanel::Get( project );
|
|
auto &undoManager = UndoManager::Get( project );
|
|
auto &window = ProjectWindow::Get( project );
|
|
|
|
if (!ProjectHistory::Get( project ).RedoAvailable()) {
|
|
AudacityMessageBox(_("Nothing to redo"));
|
|
return;
|
|
}
|
|
// Can't redo whilst dragging
|
|
if (trackPanel.IsMouseCaptured()) {
|
|
return;
|
|
}
|
|
|
|
undoManager.Redo(
|
|
[&]( const UndoState &state ){
|
|
ProjectHistory::Get( project ).PopState( state ); } );
|
|
|
|
auto t = *tracks.Selected().begin();
|
|
if (!t)
|
|
t = *tracks.Any().begin();
|
|
if (t) {
|
|
TrackFocus::Get(project).Set(t);
|
|
t->EnsureVisible();
|
|
}
|
|
}
|
|
|
|
void OnCut(const CommandContext &context)
|
|
{
|
|
auto &project = context.project;
|
|
auto &tracks = TrackList::Get( project );
|
|
auto &trackPanel = TrackPanel::Get( project );
|
|
auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;
|
|
auto &ruler = AdornedRulerPanel::Get( project );
|
|
auto &window = ProjectWindow::Get( project );
|
|
|
|
// This doesn't handle cutting labels, it handles
|
|
// cutting the _text_ inside of labels, i.e. if you're
|
|
// in the middle of editing the label text and select "Cut".
|
|
|
|
for (auto lt : tracks.Selected< LabelTrack >()) {
|
|
auto &view = LabelTrackView::Get( *lt );
|
|
if (view.CutSelectedText( context.project )) {
|
|
trackPanel.Refresh(false);
|
|
return;
|
|
}
|
|
}
|
|
|
|
auto &clipboard = Clipboard::Get();
|
|
clipboard.Clear();
|
|
|
|
auto pNewClipboard = TrackList::Create();
|
|
auto &newClipboard = *pNewClipboard;
|
|
|
|
tracks.Selected().Visit(
|
|
#if defined(USE_MIDI)
|
|
[&](NoteTrack *n) {
|
|
// Since portsmf has a built-in cut operator, we use that instead
|
|
auto dest = n->Cut(selectedRegion.t0(),
|
|
selectedRegion.t1());
|
|
FinishCopy(n, dest, newClipboard);
|
|
},
|
|
#endif
|
|
[&](Track *n) {
|
|
auto dest = n->Copy(selectedRegion.t0(),
|
|
selectedRegion.t1());
|
|
FinishCopy(n, dest, newClipboard);
|
|
}
|
|
);
|
|
|
|
// Survived possibility of exceptions. Commit changes to the clipboard now.
|
|
clipboard.Assign(
|
|
std::move( newClipboard ),
|
|
selectedRegion.t0(),
|
|
selectedRegion.t1(),
|
|
&project
|
|
);
|
|
|
|
// Proceed to change the project. If this throws, the project will be
|
|
// rolled back by the top level handler.
|
|
|
|
(tracks.Any() + &Track::IsSelectedOrSyncLockSelected).Visit(
|
|
#if defined(USE_MIDI)
|
|
[](NoteTrack*) {
|
|
//if NoteTrack, it was cut, so do not clear anything
|
|
|
|
// PRL: But what if it was sync lock selected only, not selected?
|
|
},
|
|
#endif
|
|
[&](WaveTrack *wt, const Track::Fallthrough &fallthrough) {
|
|
if (gPrefs->Read(wxT("/GUI/EnableCutLines"), (long)0)) {
|
|
wt->ClearAndAddCutLine(
|
|
selectedRegion.t0(),
|
|
selectedRegion.t1());
|
|
}
|
|
else
|
|
fallthrough();
|
|
},
|
|
[&](Track *n) {
|
|
n->Clear(selectedRegion.t0(),
|
|
selectedRegion.t1());
|
|
}
|
|
);
|
|
|
|
selectedRegion.collapseToT0();
|
|
|
|
ProjectHistory::Get( project ).PushState(_("Cut to the clipboard"), _("Cut"));
|
|
|
|
// Bug 1663
|
|
//mRuler->ClearPlayRegion();
|
|
ruler.DrawOverlays( true );
|
|
}
|
|
|
|
void OnDelete(const CommandContext &context)
|
|
{
|
|
auto &project = context.project;
|
|
auto &tracks = TrackList::Get( project );
|
|
auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;
|
|
auto &window = ProjectWindow::Get( project );
|
|
|
|
for (auto n : tracks.Any()) {
|
|
if (n->GetSelected() || n->IsSyncLockSelected()) {
|
|
n->Clear(selectedRegion.t0(), selectedRegion.t1());
|
|
}
|
|
}
|
|
|
|
double seconds = selectedRegion.duration();
|
|
|
|
selectedRegion.collapseToT0();
|
|
|
|
ProjectHistory::Get( project ).PushState(wxString::Format(_("Deleted %.2f seconds at t=%.2f"),
|
|
seconds,
|
|
selectedRegion.t0()),
|
|
_("Delete"));
|
|
}
|
|
|
|
|
|
void OnCopy(const CommandContext &context)
|
|
{
|
|
auto &project = context.project;
|
|
auto &tracks = TrackList::Get( project );
|
|
auto &trackPanel = TrackPanel::Get( project );
|
|
auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;
|
|
|
|
for (auto lt : tracks.Selected< LabelTrack >()) {
|
|
auto &view = LabelTrackView::Get( *lt );
|
|
if (view.CopySelectedText( context.project )) {
|
|
//trackPanel.Refresh(false);
|
|
return;
|
|
}
|
|
}
|
|
|
|
auto &clipboard = Clipboard::Get();
|
|
clipboard.Clear();
|
|
|
|
auto pNewClipboard = TrackList::Create();
|
|
auto &newClipboard = *pNewClipboard;
|
|
|
|
for (auto n : tracks.Selected()) {
|
|
auto dest = n->Copy(selectedRegion.t0(),
|
|
selectedRegion.t1());
|
|
FinishCopy(n, dest, newClipboard);
|
|
}
|
|
|
|
// Survived possibility of exceptions. Commit changes to the clipboard now.
|
|
clipboard.Assign( std::move( newClipboard ),
|
|
selectedRegion.t0(), selectedRegion.t1(), &project );
|
|
|
|
//Make sure the menus/toolbar states get updated
|
|
trackPanel.Refresh(false);
|
|
}
|
|
|
|
void OnPaste(const CommandContext &context)
|
|
{
|
|
auto &project = context.project;
|
|
auto &tracks = TrackList::Get( project );
|
|
auto &trackFactory = TrackFactory::Get( project );
|
|
auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;
|
|
const auto &settings = ProjectSettings::Get( project );
|
|
auto &window = ProjectWindow::Get( project );
|
|
|
|
auto isSyncLocked = settings.IsSyncLocked();
|
|
|
|
// Handle text paste (into active label) first.
|
|
if (DoPasteText(project))
|
|
return;
|
|
|
|
// If nothing's selected, we just insert NEW tracks.
|
|
if (DoPasteNothingSelected(project))
|
|
return;
|
|
|
|
const auto &clipboard = Clipboard::Get();
|
|
auto clipTrackRange = clipboard.GetTracks().Any< const Track >();
|
|
if (clipTrackRange.empty())
|
|
return;
|
|
|
|
// Otherwise, paste into the selected tracks.
|
|
double t0 = selectedRegion.t0();
|
|
double t1 = selectedRegion.t1();
|
|
|
|
auto pN = tracks.Any().begin();
|
|
|
|
Track *ff = NULL;
|
|
const Track *lastClipBeforeMismatch = NULL;
|
|
const Track *mismatchedClip = NULL;
|
|
const Track *prevClip = NULL;
|
|
|
|
bool bAdvanceClipboard = true;
|
|
bool bPastedSomething = false;
|
|
|
|
auto pC = clipTrackRange.begin();
|
|
size_t nnChannels=0, ncChannels=0;
|
|
while (*pN && *pC) {
|
|
auto n = *pN;
|
|
auto c = *pC;
|
|
if (n->GetSelected()) {
|
|
bAdvanceClipboard = true;
|
|
if (mismatchedClip)
|
|
c = mismatchedClip;
|
|
if (!c->SameKindAs(*n)) {
|
|
if (!mismatchedClip) {
|
|
lastClipBeforeMismatch = prevClip;
|
|
mismatchedClip = c;
|
|
}
|
|
bAdvanceClipboard = false;
|
|
c = lastClipBeforeMismatch;
|
|
|
|
|
|
// If the types still don't match...
|
|
while (c && !c->SameKindAs(*n)) {
|
|
prevClip = c;
|
|
c = * ++ pC;
|
|
}
|
|
}
|
|
|
|
// Handle case where the first track in clipboard
|
|
// is of different type than the first selected track
|
|
if (!c) {
|
|
c = mismatchedClip;
|
|
while (n && (!c->SameKindAs(*n) || !n->GetSelected()))
|
|
{
|
|
// Must perform sync-lock adjustment before incrementing n
|
|
if (n->IsSyncLockSelected()) {
|
|
auto newT1 = t0 + clipboard.Duration();
|
|
if (t1 != newT1 && t1 <= n->GetEndTime()) {
|
|
n->SyncLockAdjust(t1, newT1);
|
|
bPastedSomething = true;
|
|
}
|
|
}
|
|
n = * ++ pN;
|
|
}
|
|
if (!n)
|
|
c = NULL;
|
|
}
|
|
|
|
// The last possible case for cross-type pastes: triggered when we try
|
|
// to paste 1+ tracks from one type into 1+ tracks of another type. If
|
|
// there's a mix of types, this shouldn't run.
|
|
if (!c)
|
|
// Throw, so that any previous changes to the project in this loop
|
|
// are discarded.
|
|
throw SimpleMessageBoxException{
|
|
_("Pasting one type of track into another is not allowed.")
|
|
};
|
|
|
|
// We should need this check only each time we visit the leading
|
|
// channel
|
|
if ( n->IsLeader() ) {
|
|
wxASSERT( c->IsLeader() ); // the iteration logic should ensure this
|
|
|
|
auto cChannels = TrackList::Channels(c);
|
|
ncChannels = cChannels.size();
|
|
auto nChannels = TrackList::Channels(n);
|
|
nnChannels = nChannels.size();
|
|
|
|
// When trying to copy from stereo to mono track, show error and
|
|
// exit
|
|
// TODO: Automatically offer user to mix down to mono (unfortunately
|
|
// this is not easy to implement
|
|
if (ncChannels > nnChannels)
|
|
{
|
|
if (ncChannels > 2) {
|
|
// TODO: more-than-two-channels-message
|
|
// Re-word the error message
|
|
}
|
|
// else
|
|
|
|
// Throw, so that any previous changes to the project in this
|
|
// loop are discarded.
|
|
throw SimpleMessageBoxException{
|
|
_("Copying stereo audio into a mono track is not allowed.")
|
|
};
|
|
}
|
|
}
|
|
|
|
if (!ff)
|
|
ff = n;
|
|
|
|
wxASSERT( n && c && n->SameKindAs(*c) );
|
|
Maybe<WaveTrack::Locker> locker;
|
|
|
|
n->TypeSwitch(
|
|
[&](WaveTrack *wn){
|
|
const auto wc = static_cast<const WaveTrack *>(c);
|
|
if (clipboard.Project() != &project)
|
|
// Cause duplication of block files on disk, when copy is
|
|
// between projects
|
|
locker.create(wc);
|
|
bPastedSomething = true;
|
|
wn->ClearAndPaste(t0, t1, wc, true, true);
|
|
},
|
|
[&](LabelTrack *ln){
|
|
// Per Bug 293, users expect labels to move on a paste into
|
|
// a label track.
|
|
ln->Clear(t0, t1);
|
|
|
|
ln->ShiftLabelsOnInsert( clipboard.Duration(), t0 );
|
|
|
|
bPastedSomething |= ln->PasteOver(t0, c);
|
|
},
|
|
[&](Track *){
|
|
bPastedSomething = true;
|
|
n->Clear(t0, t1);
|
|
n->Paste(t0, c);
|
|
}
|
|
);
|
|
|
|
--nnChannels;
|
|
--ncChannels;
|
|
|
|
// When copying from mono to stereo track, paste the wave form
|
|
// to both channels
|
|
// TODO: more-than-two-channels
|
|
// This will replicate the last pasted channel as many times as needed
|
|
while (nnChannels > 0 && ncChannels == 0)
|
|
{
|
|
n = * ++ pN;
|
|
--nnChannels;
|
|
|
|
n->TypeSwitch(
|
|
[&](WaveTrack *wn){
|
|
bPastedSomething = true;
|
|
// Note: rely on locker being still be in scope!
|
|
wn->ClearAndPaste(t0, t1, c, true, true);
|
|
},
|
|
[&](Track *){
|
|
n->Clear(t0, t1);
|
|
bPastedSomething = true;
|
|
n->Paste(t0, c);
|
|
}
|
|
);
|
|
}
|
|
|
|
if (bAdvanceClipboard) {
|
|
prevClip = c;
|
|
c = * ++ pC;
|
|
}
|
|
} // if (n->GetSelected())
|
|
else if (n->IsSyncLockSelected())
|
|
{
|
|
auto newT1 = t0 + clipboard.Duration();
|
|
if (t1 != newT1 && t1 <= n->GetEndTime()) {
|
|
n->SyncLockAdjust(t1, newT1);
|
|
bPastedSomething = true;
|
|
}
|
|
}
|
|
++pN;
|
|
}
|
|
|
|
// This block handles the cases where our clipboard is smaller
|
|
// than the amount of selected destination tracks. We take the
|
|
// last wave track, and paste that one into the remaining
|
|
// selected tracks.
|
|
if ( *pN && ! *pC )
|
|
{
|
|
const auto wc =
|
|
*clipboard.GetTracks().Any< const WaveTrack >().rbegin();
|
|
Maybe<WaveTrack::Locker> locker;
|
|
if (clipboard.Project() != &project && wc)
|
|
// Cause duplication of block files on disk, when copy is
|
|
// between projects
|
|
locker.create(static_cast<const WaveTrack*>(wc));
|
|
|
|
tracks.Any().StartingWith(*pN).Visit(
|
|
[&](WaveTrack *wt, const Track::Fallthrough &fallthrough) {
|
|
if (!wt->GetSelected())
|
|
return fallthrough();
|
|
|
|
if (wc) {
|
|
bPastedSomething = true;
|
|
wt->ClearAndPaste(t0, t1, wc, true, true);
|
|
}
|
|
else {
|
|
auto tmp = trackFactory.NewWaveTrack(
|
|
wt->GetSampleFormat(), wt->GetRate());
|
|
tmp->InsertSilence( 0.0,
|
|
// MJS: Is this correct?
|
|
clipboard.Duration() );
|
|
tmp->Flush();
|
|
|
|
bPastedSomething = true;
|
|
wt->ClearAndPaste(t0, t1, tmp.get(), true, true);
|
|
}
|
|
},
|
|
[&](LabelTrack *lt, const Track::Fallthrough &fallthrough) {
|
|
if (!lt->GetSelected())
|
|
return fallthrough();
|
|
|
|
lt->Clear(t0, t1);
|
|
|
|
// As above, only shift labels if sync-lock is on.
|
|
if (isSyncLocked)
|
|
lt->ShiftLabelsOnInsert(
|
|
clipboard.Duration(), t0);
|
|
},
|
|
[&](Track *n) {
|
|
if (n->IsSyncLockSelected())
|
|
n->SyncLockAdjust(t1, t0 + clipboard.Duration() );
|
|
}
|
|
);
|
|
}
|
|
|
|
// TODO: What if we clicked past the end of the track?
|
|
|
|
if (bPastedSomething)
|
|
{
|
|
selectedRegion.setT1( t0 + clipboard.Duration() );
|
|
|
|
ProjectHistory::Get( project )
|
|
.PushState(_("Pasted from the clipboard"), _("Paste"));
|
|
|
|
if (ff) {
|
|
TrackFocus::Get(project).Set(ff);
|
|
ff->EnsureVisible();
|
|
}
|
|
}
|
|
}
|
|
|
|
void OnDuplicate(const CommandContext &context)
|
|
{
|
|
auto &project = context.project;
|
|
auto &tracks = TrackList::Get( project );
|
|
auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;
|
|
auto &window = ProjectWindow::Get( project );
|
|
|
|
// This iteration is unusual because we add to the list inside the loop
|
|
auto range = tracks.Selected();
|
|
auto last = *range.rbegin();
|
|
for (auto n : range) {
|
|
// Make copies not for clipboard but for direct addition to the project
|
|
auto dest = n->Copy(selectedRegion.t0(),
|
|
selectedRegion.t1(), false);
|
|
dest->Init(*n);
|
|
dest->SetOffset(wxMax(selectedRegion.t0(), n->GetOffset()));
|
|
tracks.Add( dest );
|
|
|
|
// This break is really needed, else we loop infinitely
|
|
if (n == last)
|
|
break;
|
|
}
|
|
|
|
ProjectHistory::Get( project ).PushState(_("Duplicated"), _("Duplicate"));
|
|
}
|
|
|
|
void OnSplitCut(const CommandContext &context)
|
|
{
|
|
auto &project = context.project;
|
|
auto &tracks = TrackList::Get( project );
|
|
auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;
|
|
auto &window = ProjectWindow::Get( project );
|
|
|
|
auto &clipboard = Clipboard::Get();
|
|
clipboard.Clear();
|
|
|
|
auto pNewClipboard = TrackList::Create();
|
|
auto &newClipboard = *pNewClipboard;
|
|
|
|
Track::Holder dest;
|
|
|
|
tracks.Selected().Visit(
|
|
[&](WaveTrack *n) {
|
|
dest = n->SplitCut(
|
|
selectedRegion.t0(),
|
|
selectedRegion.t1());
|
|
if (dest)
|
|
FinishCopy(n, dest, newClipboard);
|
|
},
|
|
[&](Track *n) {
|
|
dest = n->Copy(selectedRegion.t0(),
|
|
selectedRegion.t1());
|
|
n->Silence(selectedRegion.t0(),
|
|
selectedRegion.t1());
|
|
if (dest)
|
|
FinishCopy(n, dest, newClipboard);
|
|
}
|
|
);
|
|
|
|
// Survived possibility of exceptions. Commit changes to the clipboard now.
|
|
clipboard.Assign( std::move( newClipboard ),
|
|
selectedRegion.t0(), selectedRegion.t1(), &project );
|
|
|
|
ProjectHistory::Get( project ).PushState(_("Split-cut to the clipboard"), _("Split Cut"));
|
|
}
|
|
|
|
void OnSplitDelete(const CommandContext &context)
|
|
{
|
|
auto &project = context.project;
|
|
auto &tracks = TrackList::Get( project );
|
|
auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;
|
|
auto &window = ProjectWindow::Get( project );
|
|
|
|
tracks.Selected().Visit(
|
|
[&](WaveTrack *wt) {
|
|
wt->SplitDelete(selectedRegion.t0(),
|
|
selectedRegion.t1());
|
|
},
|
|
[&](Track *n) {
|
|
n->Silence(selectedRegion.t0(),
|
|
selectedRegion.t1());
|
|
}
|
|
);
|
|
|
|
ProjectHistory::Get( project ).PushState(
|
|
wxString::Format(_("Split-deleted %.2f seconds at t=%.2f"),
|
|
selectedRegion.duration(),
|
|
selectedRegion.t0()),
|
|
_("Split Delete"));
|
|
}
|
|
|
|
void OnSilence(const CommandContext &context)
|
|
{
|
|
auto &project = context.project;
|
|
auto &tracks = TrackList::Get( project );
|
|
auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;
|
|
|
|
for ( auto n : tracks.Selected< WaveTrack >() )
|
|
n->Silence(selectedRegion.t0(), selectedRegion.t1());
|
|
|
|
ProjectHistory::Get( project ).PushState(
|
|
wxString::Format(_("Silenced selected tracks for %.2f seconds at %.2f"),
|
|
selectedRegion.duration(),
|
|
selectedRegion.t0()),
|
|
_("Silence"));
|
|
}
|
|
|
|
void OnTrim(const CommandContext &context)
|
|
{
|
|
auto &project = context.project;
|
|
auto &tracks = TrackList::Get( project );
|
|
auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;
|
|
auto &window = ProjectWindow::Get( project );
|
|
|
|
if (selectedRegion.isPoint())
|
|
return;
|
|
|
|
tracks.Selected().Visit(
|
|
[&](WaveTrack *wt) {
|
|
//Delete the section before the left selector
|
|
wt->Trim(selectedRegion.t0(),
|
|
selectedRegion.t1());
|
|
}
|
|
);
|
|
|
|
ProjectHistory::Get( project ).PushState(
|
|
wxString::Format(
|
|
_("Trim selected audio tracks from %.2f seconds to %.2f seconds"),
|
|
selectedRegion.t0(), selectedRegion.t1()),
|
|
_("Trim Audio"));
|
|
}
|
|
|
|
void OnSplit(const CommandContext &context)
|
|
{
|
|
auto &project = context.project;
|
|
auto &tracks = TrackList::Get( project );
|
|
auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;
|
|
|
|
double sel0 = selectedRegion.t0();
|
|
double sel1 = selectedRegion.t1();
|
|
|
|
for (auto wt : tracks.Selected< WaveTrack >())
|
|
wt->Split( sel0, sel1 );
|
|
|
|
ProjectHistory::Get( project ).PushState(_("Split"), _("Split"));
|
|
#if 0
|
|
//ANSWER-ME: Do we need to keep this commented out OnSplit() code?
|
|
// This whole section no longer used...
|
|
/*
|
|
* Previous (pre-multiclip) implementation of "Split" command
|
|
* This does work only when a range is selected!
|
|
*
|
|
TrackListIterator iter(tracks);
|
|
|
|
Track *n = iter.First();
|
|
Track *dest;
|
|
|
|
TrackList newTracks;
|
|
|
|
while (n) {
|
|
if (n->GetSelected()) {
|
|
double sel0 = selectedRegion.t0();
|
|
double sel1 = selectedRegion.t1();
|
|
|
|
dest = n->Copy(sel0, sel1);
|
|
dest->Init(*n);
|
|
dest->SetOffset(wxMax(sel0, n->GetOffset()));
|
|
|
|
if (sel1 >= n->GetEndTime())
|
|
n->Clear(sel0, sel1);
|
|
else if (sel0 <= n->GetOffset()) {
|
|
n->Clear(sel0, sel1);
|
|
n->SetOffset(sel1);
|
|
} else
|
|
n->Silence(sel0, sel1);
|
|
|
|
newTracks.Add(dest);
|
|
}
|
|
n = iter.Next();
|
|
}
|
|
|
|
TrackListIterator nIter(&newTracks);
|
|
n = nIter.First();
|
|
while (n) {
|
|
tracks->Add(n);
|
|
n = nIter.Next();
|
|
}
|
|
|
|
PushState(_("Split"), _("Split"));
|
|
*/
|
|
#endif
|
|
}
|
|
|
|
void OnSplitNew(const CommandContext &context)
|
|
{
|
|
auto &project = context.project;
|
|
auto &tracks = TrackList::Get( project );
|
|
auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;
|
|
auto &window = ProjectWindow::Get( project );
|
|
|
|
Track::Holder dest;
|
|
|
|
// This iteration is unusual because we add to the list inside the loop
|
|
auto range = tracks.Selected();
|
|
auto last = *range.rbegin();
|
|
for (auto track : range) {
|
|
track->TypeSwitch(
|
|
[&](WaveTrack *wt) {
|
|
// Clips must be aligned to sample positions or the NEW clip will
|
|
// not fit in the gap where it came from
|
|
double offset = wt->GetOffset();
|
|
offset = wt->LongSamplesToTime(wt->TimeToLongSamples(offset));
|
|
double newt0 = wt->LongSamplesToTime(wt->TimeToLongSamples(
|
|
selectedRegion.t0()));
|
|
double newt1 = wt->LongSamplesToTime(wt->TimeToLongSamples(
|
|
selectedRegion.t1()));
|
|
dest = wt->SplitCut(newt0, newt1);
|
|
if (dest) {
|
|
dest->SetOffset(wxMax(newt0, offset));
|
|
FinishCopy(wt, dest, tracks);
|
|
}
|
|
}
|
|
#if 0
|
|
,
|
|
// LL: For now, just skip all non-wave tracks since the other do not
|
|
// yet support proper splitting.
|
|
[&](Track *n) {
|
|
dest = n->Cut(viewInfo.selectedRegion.t0(),
|
|
viewInfo.selectedRegion.t1());
|
|
if (dest) {
|
|
dest->SetOffset(wxMax(0, n->GetOffset()));
|
|
FinishCopy(n, dest, *tracks);
|
|
}
|
|
}
|
|
#endif
|
|
);
|
|
if (track == last)
|
|
break;
|
|
}
|
|
|
|
ProjectHistory::Get( project )
|
|
.PushState(_("Split to new track"), _("Split New"));
|
|
}
|
|
|
|
void OnJoin(const CommandContext &context)
|
|
{
|
|
auto &project = context.project;
|
|
auto &tracks = TrackList::Get( project );
|
|
auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;
|
|
auto &window = ProjectWindow::Get( project );
|
|
|
|
for (auto wt : tracks.Selected< WaveTrack >())
|
|
wt->Join(selectedRegion.t0(),
|
|
selectedRegion.t1());
|
|
|
|
ProjectHistory::Get( project ).PushState(
|
|
wxString::Format(_("Joined %.2f seconds at t=%.2f"),
|
|
selectedRegion.duration(),
|
|
selectedRegion.t0()),
|
|
_("Join"));
|
|
}
|
|
|
|
void OnDisjoin(const CommandContext &context)
|
|
{
|
|
auto &project = context.project;
|
|
auto &tracks = TrackList::Get( project );
|
|
auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;
|
|
auto &window = ProjectWindow::Get( project );
|
|
|
|
for (auto wt : tracks.Selected< WaveTrack >())
|
|
wt->Disjoin(selectedRegion.t0(),
|
|
selectedRegion.t1());
|
|
|
|
ProjectHistory::Get( project ).PushState(
|
|
wxString::Format(_("Detached %.2f seconds at t=%.2f"),
|
|
selectedRegion.duration(),
|
|
selectedRegion.t0()),
|
|
_("Detach"));
|
|
}
|
|
|
|
void OnEditMetadata(const CommandContext &context)
|
|
{
|
|
auto &project = context.project;
|
|
(void)Exporter::DoEditMetadata( project,
|
|
_("Edit Metadata Tags"), _("Metadata Tags"), true);
|
|
}
|
|
|
|
void OnPreferences(const CommandContext &context)
|
|
{
|
|
auto &project = context.project;
|
|
|
|
GlobalPrefsDialog dialog(&GetProjectFrame( project ) /* parent */ );
|
|
|
|
if( ScreenshotCommand::MayCapture( &dialog ) )
|
|
return;
|
|
|
|
if (!dialog.ShowModal()) {
|
|
// Canceled
|
|
return;
|
|
}
|
|
|
|
// LL: Moved from PrefsDialog since wxWidgets on OSX can't deal with
|
|
// rebuilding the menus while the PrefsDialog is still in the modal
|
|
// state.
|
|
for (auto p : AllProjects{}) {
|
|
MenuManager::Get(*p).RebuildMenuBar(*p);
|
|
// TODO: The comment below suggests this workaround is obsolete.
|
|
#if defined(__WXGTK__)
|
|
// Workaround for:
|
|
//
|
|
// http://bugzilla.audacityteam.org/show_bug.cgi?id=458
|
|
//
|
|
// This workaround should be removed when Audacity updates to wxWidgets
|
|
// 3.x which has a fix.
|
|
auto &window = GetProjectFrame( *p );
|
|
wxRect r = window.GetRect();
|
|
window.SetSize(wxSize(1,1));
|
|
window.SetSize(r.GetSize());
|
|
#endif
|
|
}
|
|
}
|
|
|
|
// Legacy functions, not used as of version 2.3.0
|
|
|
|
#if 0
|
|
void OnPasteOver(const CommandContext &context)
|
|
{
|
|
auto &project = context.project;
|
|
auto &selectedRegion = project.GetViewInfo().selectedRegion;
|
|
|
|
if((AudacityProject::msClipT1 - AudacityProject::msClipT0) > 0.0)
|
|
{
|
|
selectedRegion.setT1(
|
|
selectedRegion.t0() +
|
|
(AudacityProject::msClipT1 - AudacityProject::msClipT0));
|
|
// MJS: pointless, given what we do in OnPaste?
|
|
}
|
|
OnPaste(context);
|
|
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
}; // struct Handler
|
|
|
|
} // namespace
|
|
|
|
static CommandHandlerObject &findCommandHandler(AudacityProject &) {
|
|
// Handler is not stateful. Doesn't need a factory registered with
|
|
// AudacityProject.
|
|
static EditActions::Handler instance;
|
|
return instance;
|
|
};
|
|
|
|
// Menu definitions
|
|
|
|
#define FN(X) findCommandHandler, \
|
|
static_cast<CommandFunctorPointer>(& EditActions::Handler :: X)
|
|
#define XXO(X) _(X), wxString{X}.Contains("...")
|
|
|
|
MenuTable::BaseItemPtr LabelEditMenus( AudacityProject &project );
|
|
|
|
const ReservedCommandFlag
|
|
CutCopyAvailableFlag{
|
|
[](const AudacityProject &project){
|
|
auto range = TrackList::Get( project ).Any<const LabelTrack>()
|
|
+ [&](const LabelTrack *pTrack){
|
|
return LabelTrackView::Get( *pTrack ).IsTextSelected(
|
|
// unhappy const_cast because track focus might be set
|
|
const_cast<AudacityProject&>(project)
|
|
);
|
|
};
|
|
if ( !range.empty() )
|
|
return true;
|
|
|
|
if (
|
|
TimeSelectedPred( project )
|
|
&&
|
|
TracksSelectedPred( project )
|
|
)
|
|
return true;
|
|
|
|
return false;
|
|
},
|
|
cutCopyOptions
|
|
};
|
|
|
|
MenuTable::BaseItemPtr EditMenu( AudacityProject & )
|
|
{
|
|
using namespace MenuTable;
|
|
using Options = CommandManager::Options;
|
|
|
|
static const auto NotBusyTimeAndTracksFlags =
|
|
AudioIONotBusyFlag | TimeSelectedFlag | TracksSelectedFlag;
|
|
|
|
// The default shortcut key for Redo is different on different platforms.
|
|
static constexpr auto redoKey =
|
|
#ifdef __WXMSW__
|
|
wxT("Ctrl+Y")
|
|
#else
|
|
wxT("Ctrl+Shift+Z")
|
|
#endif
|
|
;
|
|
|
|
// The default shortcut key for Preferences is different on different
|
|
// platforms.
|
|
static constexpr auto prefKey =
|
|
#ifdef __WXMAC__
|
|
wxT("Ctrl+,")
|
|
#else
|
|
wxT("Ctrl+P")
|
|
#endif
|
|
;
|
|
|
|
return Menu( _("&Edit"),
|
|
Command( wxT("Undo"), XXO("&Undo"), FN(OnUndo),
|
|
AudioIONotBusyFlag | UndoAvailableFlag, wxT("Ctrl+Z") ),
|
|
|
|
Command( wxT("Redo"), XXO("&Redo"), FN(OnRedo),
|
|
AudioIONotBusyFlag | RedoAvailableFlag, redoKey ),
|
|
|
|
Special( [](AudacityProject &project, wxMenu&) {
|
|
// Change names in the CommandManager as a side-effect
|
|
MenuManager::ModifyUndoMenuItems(project);
|
|
}),
|
|
|
|
Separator(),
|
|
|
|
// Basic Edit commands
|
|
/* i18n-hint: (verb)*/
|
|
Command( wxT("Cut"), XXO("Cu&t"), FN(OnCut),
|
|
AudioIONotBusyFlag | CutCopyAvailableFlag | NoAutoSelect,
|
|
wxT("Ctrl+X") ),
|
|
Command( wxT("Delete"), XXO("&Delete"), FN(OnDelete),
|
|
AudioIONotBusyFlag | TracksSelectedFlag | TimeSelectedFlag | NoAutoSelect,
|
|
wxT("Ctrl+K") ),
|
|
/* i18n-hint: (verb)*/
|
|
Command( wxT("Copy"), XXO("&Copy"), FN(OnCopy),
|
|
AudioIONotBusyFlag | CutCopyAvailableFlag, wxT("Ctrl+C") ),
|
|
/* i18n-hint: (verb)*/
|
|
Command( wxT("Paste"), XXO("&Paste"), FN(OnPaste),
|
|
AudioIONotBusyFlag, wxT("Ctrl+V") ),
|
|
/* i18n-hint: (verb)*/
|
|
Command( wxT("Duplicate"), XXO("Duplic&ate"), FN(OnDuplicate),
|
|
NotBusyTimeAndTracksFlags, wxT("Ctrl+D") ),
|
|
|
|
Separator(),
|
|
|
|
Menu( _("R&emove Special"),
|
|
/* i18n-hint: (verb) Do a special kind of cut*/
|
|
Command( wxT("SplitCut"), XXO("Spl&it Cut"), FN(OnSplitCut),
|
|
NotBusyTimeAndTracksFlags,
|
|
Options{ wxT("Ctrl+Alt+X") }.UseStrictFlags() ),
|
|
/* i18n-hint: (verb) Do a special kind of DELETE*/
|
|
Command( wxT("SplitDelete"), XXO("Split D&elete"), FN(OnSplitDelete),
|
|
NotBusyTimeAndTracksFlags,
|
|
Options{ wxT("Ctrl+Alt+K") }.UseStrictFlags() ),
|
|
|
|
Separator(),
|
|
|
|
/* i18n-hint: (verb)*/
|
|
Command( wxT("Silence"), XXO("Silence Audi&o"), FN(OnSilence),
|
|
AudioIONotBusyFlag | TimeSelectedFlag | WaveTracksSelectedFlag,
|
|
wxT("Ctrl+L") ),
|
|
/* i18n-hint: (verb)*/
|
|
Command( wxT("Trim"), XXO("Tri&m Audio"), FN(OnTrim),
|
|
AudioIONotBusyFlag | TimeSelectedFlag | WaveTracksSelectedFlag,
|
|
Options{ wxT("Ctrl+T") }.UseStrictFlags() )
|
|
),
|
|
|
|
Separator(),
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
|
|
Menu( _("Clip B&oundaries"),
|
|
/* i18n-hint: (verb) It's an item on a menu. */
|
|
Command( wxT("Split"), XXO("Sp&lit"), FN(OnSplit),
|
|
AudioIONotBusyFlag | WaveTracksSelectedFlag,
|
|
Options{ wxT("Ctrl+I") }.UseStrictFlags() ),
|
|
Command( wxT("SplitNew"), XXO("Split Ne&w"), FN(OnSplitNew),
|
|
AudioIONotBusyFlag | TimeSelectedFlag | WaveTracksSelectedFlag,
|
|
Options{ wxT("Ctrl+Alt+I") }.UseStrictFlags() ),
|
|
|
|
Separator(),
|
|
|
|
/* i18n-hint: (verb)*/
|
|
Command( wxT("Join"), XXO("&Join"), FN(OnJoin),
|
|
NotBusyTimeAndTracksFlags, wxT("Ctrl+J") ),
|
|
Command( wxT("Disjoin"), XXO("Detac&h at Silences"), FN(OnDisjoin),
|
|
NotBusyTimeAndTracksFlags, wxT("Ctrl+Alt+J") )
|
|
),
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
|
|
LabelEditMenus,
|
|
|
|
Command( wxT("EditMetaData"), XXO("&Metadata..."), FN(OnEditMetadata),
|
|
AudioIONotBusyFlag ),
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
|
|
#ifndef __WXMAC__
|
|
Separator(),
|
|
#endif
|
|
|
|
Command( wxT("Preferences"), XXO("Pre&ferences..."), FN(OnPreferences),
|
|
AudioIONotBusyFlag, prefKey )
|
|
);
|
|
}
|
|
|
|
MenuTable::BaseItemPtr ExtraEditMenu( AudacityProject & )
|
|
{
|
|
using namespace MenuTable;
|
|
using Options = CommandManager::Options;
|
|
static const auto flags =
|
|
AudioIONotBusyFlag | TracksSelectedFlag | TimeSelectedFlag;
|
|
return Menu( _("&Edit"),
|
|
Command( wxT("DeleteKey"), XXO("&Delete Key"), FN(OnDelete),
|
|
(flags | NoAutoSelect),
|
|
wxT("Backspace") ),
|
|
Command( wxT("DeleteKey2"), XXO("Delete Key&2"), FN(OnDelete),
|
|
(flags | NoAutoSelect),
|
|
wxT("Delete") )
|
|
);
|
|
}
|
|
|
|
auto canSelectAll = [](const AudacityProject &project){
|
|
return MenuManager::Get( project ).mWhatIfNoSelection != 0; };
|
|
auto selectAll = []( AudacityProject &project, CommandFlag flagsRqd ){
|
|
if ( MenuManager::Get( project ).mWhatIfNoSelection == 1 &&
|
|
(flagsRqd & NoAutoSelect).none() )
|
|
SelectUtilities::DoSelectAllAudio(project);
|
|
};
|
|
|
|
RegisteredMenuItemEnabler selectTracks{{
|
|
[]{ return TracksExistFlag; },
|
|
[]{ return TracksSelectedFlag; },
|
|
canSelectAll,
|
|
selectAll
|
|
}};
|
|
|
|
// Including time tracks.
|
|
RegisteredMenuItemEnabler selectAnyTracks{{
|
|
[]{ return TracksExistFlag; },
|
|
[]{ return AnyTracksSelectedFlag; },
|
|
canSelectAll,
|
|
selectAll
|
|
}};
|
|
|
|
RegisteredMenuItemEnabler selectWaveTracks{{
|
|
[]{ return WaveTracksExistFlag; },
|
|
[]{ return TimeSelectedFlag | WaveTracksSelectedFlag | CutCopyAvailableFlag; },
|
|
canSelectAll,
|
|
selectAll
|
|
}};
|
|
|
|
// Also enable select for the noise reduction case.
|
|
RegisteredMenuItemEnabler selectWaveTracks2{{
|
|
[]{ return WaveTracksExistFlag; },
|
|
[]{ return NoiseReductionTimeSelectedFlag | WaveTracksSelectedFlag | CutCopyAvailableFlag; },
|
|
canSelectAll,
|
|
selectAll
|
|
}};
|
|
|
|
#undef XXO
|
|
#undef FN
|