diff --git a/src/CommonCommandFlags.cpp b/src/CommonCommandFlags.cpp index 2cb2df38b..3e2b4edd4 100644 --- a/src/CommonCommandFlags.cpp +++ b/src/CommonCommandFlags.cpp @@ -41,16 +41,16 @@ cycles. */ -// Really means, some track is selected, that isn't a time track -bool TracksSelectedPred( const AudacityProject &project ) +// Strong predicate excludes tracks that do not support basic editing. +bool EditableTracksSelectedPred( const AudacityProject &project ) { auto range = TrackList::Get( project ).Selected() - []( const Track *pTrack ){ - return track_cast( pTrack ); }; + return !pTrack->SupportsBasicEditing(); }; return !range.empty(); }; -// This predicate includes time tracks too. +// Weaker predicate. bool AnyTracksSelectedPred( const AudacityProject &project ) { auto range = TrackList::Get( project ).Selected(); @@ -177,8 +177,8 @@ const ReservedCommandFlag& CommandFlagOptions{}.DisableDefaultMessage() }; return flag; } const ReservedCommandFlag& - TracksSelectedFlag() { static ReservedCommandFlag flag{ - TracksSelectedPred, // exclude TimeTracks + EditableTracksSelectedFlag() { static ReservedCommandFlag flag{ + EditableTracksSelectedPred, { []( const TranslatableString &Name ){ return // i18n-hint: %s will be replaced by the name of an action, such as "Remove Tracks". XO("\"%s\" requires one or more tracks to be selected.").Format( Name ); @@ -186,7 +186,7 @@ const ReservedCommandFlag& }; return flag; } const ReservedCommandFlag& AnyTracksSelectedFlag() { static ReservedCommandFlag flag{ - AnyTracksSelectedPred, // Allow TimeTracks + AnyTracksSelectedPred, { []( const TranslatableString &Name ){ return // i18n-hint: %s will be replaced by the name of an action, such as "Remove Tracks". XO("\"%s\" requires one or more tracks to be selected.").Format( Name ); diff --git a/src/CommonCommandFlags.h b/src/CommonCommandFlags.h index 357aab5c0..47ea21c63 100644 --- a/src/CommonCommandFlags.h +++ b/src/CommonCommandFlags.h @@ -15,7 +15,7 @@ Paul Licameli split from Menus.cpp #include "commands/CommandFlag.h" -bool TracksSelectedPred( const AudacityProject &project ); +bool EditableTracksSelectedPred( const AudacityProject &project ); bool AudioIOBusyPred( const AudacityProject &project ); bool TimeSelectedPred( const AudacityProject &project ); extern const CommandFlagOptions &cutCopyOptions(); @@ -27,7 +27,7 @@ extern AUDACITY_DLL_API const ReservedCommandFlag &TimeSelectedFlag(), // This is equivalent to check if there is a valid selection, so it's used for Zoom to Selection too &WaveTracksSelectedFlag(), &TracksExistFlag(), - &TracksSelectedFlag(), + &EditableTracksSelectedFlag(), &AnyTracksSelectedFlag(), &TrackPanelHasFocus(); //lll diff --git a/src/LabelTrack.cpp b/src/LabelTrack.cpp index 61a6e9e02..0d901b900 100644 --- a/src/LabelTrack.cpp +++ b/src/LabelTrack.cpp @@ -85,6 +85,13 @@ LabelTrack::LabelTrack(const LabelTrack &orig) : } } +Track::Holder LabelTrack::PasteInto( AudacityProject & ) const +{ + auto pNewTrack = std::make_shared(); + pNewTrack->Paste(0.0, this); + return pNewTrack; +} + template static IntervalType DoMakeInterval(const LabelStruct &label, size_t index) { diff --git a/src/LabelTrack.h b/src/LabelTrack.h index b10f12c81..476db8861 100644 --- a/src/LabelTrack.h +++ b/src/LabelTrack.h @@ -155,6 +155,8 @@ public: int FindNextLabel(const SelectedRegion& currentSelection); int FindPrevLabel(const SelectedRegion& currentSelection); + Track::Holder PasteInto( AudacityProject & ) const override; + struct IntervalData final : Track::IntervalData { size_t index; explicit IntervalData(size_t index) : index{index} {}; diff --git a/src/NoteTrack.cpp b/src/NoteTrack.cpp index 789a1ff8e..c73d4b83a 100644 --- a/src/NoteTrack.cpp +++ b/src/NoteTrack.cpp @@ -683,6 +683,13 @@ QuantizedTimeAndBeat NoteTrack::NearestBeatTime( double time ) const return { seq_time + GetOffset(), beat }; } +Track::Holder NoteTrack::PasteInto( AudacityProject & ) const +{ + auto pNewTrack = std::make_shared(); + pNewTrack->Paste(0.0, this); + return pNewTrack; +} + auto NoteTrack::GetIntervals() const -> ConstIntervals { ConstIntervals results; diff --git a/src/NoteTrack.h b/src/NoteTrack.h index f1cf21658..ceb7c0c0f 100644 --- a/src/NoteTrack.h +++ b/src/NoteTrack.h @@ -185,6 +185,8 @@ public: mVisibleChannels = CHANNEL_BIT(c); } + Track::Holder PasteInto( AudacityProject & ) const override; + ConstIntervals GetIntervals() const override; Intervals GetIntervals() override; diff --git a/src/ProjectFileIO.cpp b/src/ProjectFileIO.cpp index 9dc921558..6ffe36f9f 100644 --- a/src/ProjectFileIO.cpp +++ b/src/ProjectFileIO.cpp @@ -27,7 +27,6 @@ Paul Licameli split from AudacityProject.cpp #include "SampleBlock.h" #include "Tags.h" #include "TempDirectory.h" -#include "TimeTrack.h" #include "ViewInfo.h" #include "WaveTrack.h" #include "widgets/AudacityMessageBox.h" @@ -1677,357 +1676,6 @@ bool ProjectFileIO::WriteDoc(const char *table, return true; } -// Importing an AUP3 project into an AUP3 project is a bit different than -// normal importing since we need to copy data from one DB to the other -// while adjusting the sample block IDs to represent the newly assigned -// IDs. -bool ProjectFileIO::ImportProject(const FilePath &fileName) -{ - // Get access to the current project file - auto db = DB(); - - bool success = false; - bool restore = true; - int rc = SQLITE_OK; - - // Ensure the inbound database gets detached - auto detach = finally([&] - { - // If this DETACH fails, subsequent project imports will fail until Audacity - // is relaunched. - auto result = sqlite3_exec(db, "DETACH DATABASE inbound;", nullptr, nullptr, nullptr); - - // Only capture the error if there wasn't a previous error - if (result != SQLITE_OK && (rc == SQLITE_DONE || rc == SQLITE_OK)) - { - SetDBError( - XO("Failed to detach project during import") - ); - } - }); - - // Attach the inbound project file - wxString sql; - sql.Printf("ATTACH DATABASE 'file:%s?immutable=1&mode=ro' AS inbound;", fileName); - - rc = sqlite3_exec(db, sql, nullptr, nullptr, nullptr); - if (rc != SQLITE_OK) - { - SetDBError( - XO("Unable to attach %s project file").Format(fileName) - ); - - return false; - } - - // We need either the autosave or project docs from the inbound AUP3 - wxMemoryBuffer buffer; - - // Get the autosave doc, if any - if (!GetBlob("SELECT dict || doc FROM inbound.project WHERE id = 1;", buffer)) - { - // Error already set - return false; - } - - // If we didn't have an autosave doc, load the project doc instead - if (buffer.GetDataLen() == 0) - { - if (!GetBlob("SELECT dict || doc FROM inbound.autosave WHERE id = 1;", buffer)) - { - // Error already set - return false; - } - - // Missing both the autosave and project docs. This can happen if the - // system were to crash before the first autosave into a temporary file. - if (buffer.GetDataLen() == 0) - { - SetError(XO("Unable to load project or autosave documents")); - return false; - } - } - - wxString project; - - project = ProjectSerializer::Decode(buffer); - if (project.size() == 0) - { - SetError(XO("Unable to decode project document")); - - return false; - } - - // Parse the project doc - wxStringInputStream in(project); - wxXmlDocument doc; - if (!doc.Load(in)) - { - SetError(XO("Unable to parse the project document")); - - return false; - } - - // Get the root ("project") node - wxXmlNode *root = doc.GetRoot(); - if (!root->GetName().IsSameAs(wxT("project"))) - { - SetError(XO("Missing root node in project document")); - - return false; - } - - // Soft delete all non-essential attributes to prevent updating the active - // project. This takes advantage of the knowledge that when a project is - // parsed, unrecognized attributes are simply ignored. - // - // This is necessary because we don't want any of the active project settings - // to be modified by the inbound project. - for (wxXmlAttribute *attr = root->GetAttributes(); attr; attr = attr->GetNext()) - { - wxString name = attr->GetName(); - if (!name.IsSameAs(wxT("version")) && !name.IsSameAs(wxT("audacityversion"))) - { - attr->SetName(name + wxT("_deleted")); - } - } - - // Recursively find and collect all waveblock nodes - std::vector blocknodes; - std::function findblocks = [&](wxXmlNode *node) - { - while (node) - { - if (node->GetName().IsSameAs(wxT("waveblock"))) - { - blocknodes.push_back(node); - } - else - { - findblocks(node->GetChildren()); - } - - node = node->GetNext(); - } - }; - - // Get access to the active tracklist - auto pProject = &mProject; - auto &tracklist = TrackList::Get(*pProject); - - // Search for a timetrack and remove it if the project already has one - if (*tracklist.Any().begin()) - { - // Find a timetrack and remove it if it exists - for (wxXmlNode *node = doc.GetRoot()->GetChildren(); node; node = node->GetNext()) - { - if (node->GetName().IsSameAs(wxT("timetrack"))) - { - AudacityMessageBox( - XO("The active project already has a time track and one was encountered in the project being imported, bypassing imported time track."), - XO("Project Import"), - wxOK | wxICON_EXCLAMATION | wxCENTRE, - &GetProjectFrame(*pProject)); - - root->RemoveChild(node); - break; - } - } - } - - // Find all waveblocks in all wavetracks - for (wxXmlNode *node = doc.GetRoot()->GetChildren(); node; node = node->GetNext()) - { - if (node->GetName().IsSameAs(wxT("wavetrack"))) - { - findblocks(node->GetChildren()); - } - } - - { - // Cleanup... - sqlite3_stmt *stmt = nullptr; - auto cleanup = finally([&] - { - // Ensure the prepared statement gets cleaned up - if (stmt) - { - // Ignore the return code since it will have already been captured below. - sqlite3_finalize(stmt); - } - }); - - // Prepare the statement to copy the sample block from the inbound project to the - // active project. All columns other than the blockid column get copied. - wxString columns(wxT("sampleformat, summin, summax, sumrms, summary256, summary64k, samples")); - sql.Printf("INSERT INTO main.sampleblocks (%s)" - " SELECT %s" - " FROM inbound.sampleblocks" - " WHERE blockid = ?;", - columns, - columns); - - rc = sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr); - if (rc != SQLITE_OK) - { - SetDBError( - XO("Unable to prepare project file command:\n\n%s").Format(sql) - ); - return false; - } - - /* i18n-hint: This title appears on a dialog that indicates the progress - in doing something.*/ - ProgressDialog progress(XO("Progress"), XO("Importing project"), pdlgHideStopButton); - ProgressResult result = ProgressResult::Success; - - wxLongLong_t count = 0; - wxLongLong_t total = blocknodes.size(); - - rc = sqlite3_exec(db, "BEGIN;", nullptr, nullptr, nullptr); - if (rc != SQLITE_OK) - { - SetDBError( - XO("Unable to start a transaction during import") - ); - return false; - } - - // Copy all the sample blocks from the inbound project file into - // the active one, while remembering which were copied. - for (auto node : blocknodes) - { - // Find the blockid attribute...it should always be there - wxXmlAttribute *attr = node->GetAttributes(); - while (attr && !attr->GetName().IsSameAs(wxT("blockid"))) - { - attr = attr->GetNext(); - } - wxASSERT(attr != nullptr); - - // And get the blockid - SampleBlockID blockid; - attr->GetValue().ToLongLong(&blockid); - - if ( blockid <= 0 ) - // silent block - continue; - - // Bind statement parameters - rc = sqlite3_bind_int64(stmt, 1, blockid); - if (rc != SQLITE_OK) - { - SetDBError( - XO("Failed to bind SQL parameter") - ); - - break; - } - - // Process it - rc = sqlite3_step(stmt); - if (rc != SQLITE_DONE) - { - SetDBError( - XO("Failed to import sample block.\nThe following command failed:\n\n%s").Format(sql) - ); - - break; - } - - // Replace the original blockid with the new one - attr->SetValue(wxString::Format(wxT("%lld"), sqlite3_last_insert_rowid(db))); - - // Reset the statement for the next iteration - if (sqlite3_reset(stmt) != SQLITE_OK) - { - THROW_INCONSISTENCY_EXCEPTION; - } - - // Remember that we copied this node in case the user cancels - result = progress.Update(++count, total); - if (result != ProgressResult::Success) - { - break; - } - } - - // Bail if the import was cancelled or failed. If the user stopped the - // import or it completed, then we continue on. - if (rc != SQLITE_DONE || result == ProgressResult::Cancelled || result == ProgressResult::Failed) - { - // If this fails (probably due to memory or disk space), the transaction will - // (presumably) stil be active, so further updates to the project file will - // fail as well. Not really much we can do about it except tell the user. - auto result = sqlite3_exec(db, "ROLLBACK;", nullptr, nullptr, nullptr); - - // Only capture the error if there wasn't a previous error - if (result != SQLITE_OK && rc == SQLITE_DONE) - { - SetDBError( - XO("Failed to rollback transaction during import") - ); - } - - return false; - } - - // Go ahead and commit now. If this fails (probably due to memory or disk space), - // the transaction will (presumably) stil be active, so further updates to the - // project file will fail as well. Not really much we can do about it except tell - // the user. - rc = sqlite3_exec(db, "COMMIT;", nullptr, nullptr, nullptr); - if (rc != SQLITE_OK) - { - SetDBError( - XO("Unable to commit transaction during import") - ); - - return false; - } - - // Copy over tags...likely to produce duplicates...needs work once used - rc = sqlite3_exec(db, - "INSERT INTO main.tags SELECT * FROM inbound.tags;", - nullptr, - nullptr, - nullptr); - if (rc != SQLITE_OK) - { - SetDBError( - XO("Failed to import tags") - ); - - return false; - } - } - - // Recreate the project doc with the revisions we've made above. If this fails - // it's probably due to memory. - wxStringOutputStream output; - if (!doc.Save(output)) - { - SetError( - XO("Unable to recreate project information.") - ); - - return false; - } - - // Now load the document as normal - XMLFileReader xmlFile; - if (!xmlFile.ParseString(this, output.GetString())) - { - SetError( - XO("Unable to parse project information."), xmlFile.GetErrorStr() - ); - - return false; - } - - return true; -} - bool ProjectFileIO::LoadProject(const FilePath &fileName, bool ignoreAutosave) { bool success = false; diff --git a/src/ProjectFileIO.h b/src/ProjectFileIO.h index e99822917..b69cb0b1c 100644 --- a/src/ProjectFileIO.h +++ b/src/ProjectFileIO.h @@ -92,7 +92,6 @@ public: bool CloseProject(); bool ReopenProject(); - bool ImportProject(const FilePath &fileName); bool LoadProject(const FilePath &fileName, bool ignoreAutosave); bool UpdateSaved(const TrackList *tracks = nullptr); bool SaveProject(const FilePath &fileName, const TrackList *lastSaved); diff --git a/src/ProjectFileManager.cpp b/src/ProjectFileManager.cpp index 9df1452cd..c6054fbfc 100644 --- a/src/ProjectFileManager.cpp +++ b/src/ProjectFileManager.cpp @@ -1137,6 +1137,29 @@ ProjectFileManager::AddImportedTracks(const FilePath &fileName, // HandleResize(); } +namespace { +bool ImportProject(AudacityProject &dest, const FilePath &fileName) +{ + InvisibleTemporaryProject temp; + auto &project = temp.Project(); + + auto &projectFileIO = ProjectFileIO::Get(project); + if (!projectFileIO.LoadProject(fileName, false)) + return false; + auto &srcTracks = TrackList::Get(project); + auto &destTracks = TrackList::Get(dest); + for (const Track *pTrack : srcTracks.Any()) { + auto destTrack = pTrack->PasteInto(dest); + Track::FinishCopy(pTrack, destTrack.get()); + if (destTrack.use_count() == 1) + destTracks.Add(destTrack); + } + Tags::Get(dest).Merge(Tags::Get(project)); + + return true; +} +} + // If pNewTrackList is passed in non-NULL, it gets filled with the pointers to NEW tracks. bool ProjectFileManager::Import( const FilePath &fileName, @@ -1151,7 +1174,7 @@ bool ProjectFileManager::Import( // Handle AUP3 ("project") files directly if (fileName.AfterLast('.').IsSameAs(wxT("aup3"), false)) { - if (projectFileIO.ImportProject(fileName)) { + if (ImportProject(project, fileName)) { auto &history = ProjectHistory::Get(project); // If the project was clean and temporary (not permanently saved), then set diff --git a/src/SelectUtilities.cpp b/src/SelectUtilities.cpp index db0db6037..f61e6e2d0 100644 --- a/src/SelectUtilities.cpp +++ b/src/SelectUtilities.cpp @@ -85,7 +85,7 @@ void SelectAllIfNone( AudacityProject &project ) { auto &viewInfo = ViewInfo::Get( project ); auto flags = MenuManager::Get( project ).GetUpdateFlags(); - if((flags & TracksSelectedFlag()).none() || + if((flags & EditableTracksSelectedFlag()).none() || viewInfo.selectedRegion.isPoint()) DoSelectAllAudio( project ); } @@ -98,7 +98,7 @@ bool SelectAllIfNoneAndAllowed( AudacityProject &project ) auto &viewInfo = ViewInfo::Get( project ); auto flags = MenuManager::Get( project ).GetUpdateFlags(); - if((flags & TracksSelectedFlag()).none() || + if((flags & EditableTracksSelectedFlag()).none() || viewInfo.selectedRegion.isPoint()) { if (!allowed) { return false; diff --git a/src/Tags.cpp b/src/Tags.cpp index 6595f863a..c98c92e0c 100644 --- a/src/Tags.cpp +++ b/src/Tags.cpp @@ -268,6 +268,13 @@ std::shared_ptr Tags::Duplicate() const return std::make_shared(*this); } +void Tags::Merge( const Tags &other ) +{ + for ( auto &pair : other.mMap ) { + SetTag( pair.first, pair.second ); + } +} + Tags & Tags::operator=(const Tags & src) { mEditTitle = src.mEditTitle; diff --git a/src/Tags.h b/src/Tags.h index e973182e3..2210dc0e1 100644 --- a/src/Tags.h +++ b/src/Tags.h @@ -88,6 +88,8 @@ class AUDACITY_DLL_API Tags final std::shared_ptr Duplicate() const; + void Merge( const Tags &other ); + Tags & operator= (const Tags & src ); bool ShowEditDialog( diff --git a/src/TimeTrack.cpp b/src/TimeTrack.cpp index c163fe58d..4a8eeefb6 100644 --- a/src/TimeTrack.cpp +++ b/src/TimeTrack.cpp @@ -52,6 +52,11 @@ static ProjectFileIORegistry::Entry registerFactory{ TimeTrack::TimeTrack(const ZoomInfo *zoomInfo): Track() , mZoomInfo(zoomInfo) +{ + CleanState(); +} + +void TimeTrack::CleanState() { mEnvelope = std::make_unique(true, TIMETRACK_MIN, TIMETRACK_MAX, 1.0); @@ -131,6 +136,31 @@ void TimeTrack::SetRangeUpper(double upper) mEnvelope->SetRangeUpper( upper ); } +bool TimeTrack::SupportsBasicEditing() const +{ + return false; +} + +Track::Holder TimeTrack::PasteInto( AudacityProject &project ) const +{ + // Maintain uniqueness of the time track! + std::shared_ptr pNewTrack; + if( auto pTrack = *TrackList::Get( project ).Any().begin() ) + pNewTrack = pTrack->SharedPointer(); + else + pNewTrack = std::make_shared( &ViewInfo::Get( project ) ); + + // Should come here only for .aup3 import, not for paste (because the + // track is skipped in cut/copy commands) + // And for import we agree to replace the track contents completely + pNewTrack->CleanState(); + pNewTrack->Init(*this); + pNewTrack->Paste(0.0, this); + pNewTrack->SetRangeLower(this->GetRangeLower()); + pNewTrack->SetRangeUpper(this->GetRangeUpper()); + return pNewTrack; +} + Track::Holder TimeTrack::Cut( double t0, double t1 ) { auto result = Copy( t0, t1, false ); diff --git a/src/TimeTrack.h b/src/TimeTrack.h index 6125ea9a6..c2c40c434 100644 --- a/src/TimeTrack.h +++ b/src/TimeTrack.h @@ -40,6 +40,10 @@ class TimeTrack final : public Track { virtual ~TimeTrack(); + bool SupportsBasicEditing() const override; + + Holder PasteInto( AudacityProject & ) const override; + Holder Cut( double t0, double t1 ) override; Holder Copy( double t0, double t1, bool forClipboard ) const override; void Clear(double t0, double t1) override; @@ -91,6 +95,8 @@ class TimeTrack final : public Track { Ruler &GetRuler() const { return *mRuler; } private: + void CleanState(); + // Identifying the type of track TrackKind GetKind() const override { return TrackKind::Time; } diff --git a/src/Track.cpp b/src/Track.cpp index cabe7246b..fb4936bfb 100644 --- a/src/Track.cpp +++ b/src/Track.cpp @@ -1224,6 +1224,11 @@ std::shared_ptr Track::SubstituteOriginalTrack() const return SharedPointer(); } +bool Track::SupportsBasicEditing() const +{ + return true; +} + auto Track::GetIntervals() const -> ConstIntervals { return {}; diff --git a/src/Track.h b/src/Track.h index 4ee4aebc5..3adbf1b5a 100644 --- a/src/Track.h +++ b/src/Track.h @@ -318,6 +318,15 @@ class AUDACITY_DLL_API Track /* not final */ using ConstInterval = ConstTrackInterval; using ConstIntervals = std::vector< ConstInterval >; + //! Whether this track type implements cut-copy-paste; by default, true + virtual bool SupportsBasicEditing() const; + + using Holder = std::shared_ptr; + + //! Find or create the destination track for a paste, maybe in a different project + /*! @return A smart pointer to the track; its `use_count()` can tell whether it is new */ + virtual Holder PasteInto( AudacityProject & ) const = 0; + //! Report times on the track where important intervals begin and end, for UI to snap to /*! Some intervals may be empty, and no ordering of the intervals is assumed. @@ -390,7 +399,6 @@ private: void Init(const Track &orig); - using Holder = std::shared_ptr; // public nonvirtual duplication function that invokes Clone(): virtual Holder Duplicate() const; diff --git a/src/WaveTrack.cpp b/src/WaveTrack.cpp index 9d697f73c..e5d794916 100644 --- a/src/WaveTrack.cpp +++ b/src/WaveTrack.cpp @@ -334,6 +334,15 @@ static Container MakeIntervals(const std::vector &clips) return result; } +Track::Holder WaveTrack::PasteInto( AudacityProject &project ) const +{ + auto &trackFactory = WaveTrackFactory::Get( project ); + auto &pSampleBlockFactory = trackFactory.GetSampleBlockFactory(); + auto pNewTrack = EmptyCopy( pSampleBlockFactory ); + pNewTrack->Paste(0.0, this); + return pNewTrack; +} + auto WaveTrack::GetIntervals() const -> ConstIntervals { return MakeIntervals( mClips ); diff --git a/src/WaveTrack.h b/src/WaveTrack.h index 370c1ff73..97e8c2e53 100644 --- a/src/WaveTrack.h +++ b/src/WaveTrack.h @@ -537,6 +537,8 @@ private: std::shared_ptr pClip; }; + Track::Holder PasteInto( AudacityProject & ) const override; + ConstIntervals GetIntervals() const override; Intervals GetIntervals() override; diff --git a/src/menus/EditMenus.cpp b/src/menus/EditMenus.cpp index ea9eb1f80..169873416 100644 --- a/src/menus/EditMenus.cpp +++ b/src/menus/EditMenus.cpp @@ -11,7 +11,6 @@ #include "../ProjectSettings.h" #include "../ProjectWindow.h" #include "../SelectUtilities.h" -#include "../TimeTrack.h" #include "../TrackPanel.h" #include "../TrackPanelAx.h" #include "../UndoManager.h" @@ -76,8 +75,6 @@ bool DoPasteText(AudacityProject &project) bool DoPasteNothingSelected(AudacityProject &project) { auto &tracks = TrackList::Get( project ); - auto &trackFactory = WaveTrackFactory::Get( project ); - auto &pSampleBlockFactory = trackFactory.GetSampleBlockFactory(); auto &selectedRegion = ViewInfo::Get( project ).selectedRegion; auto &viewInfo = ViewInfo::Get( project ); auto &window = ProjectWindow::Get( project ); @@ -94,44 +91,18 @@ bool DoPasteNothingSelected(AudacityProject &project) Track* pFirstNewTrack = NULL; for (auto pClip : clipTrackRange) { - Track::Holder uNewTrack; - Track *pNewTrack; - pClip->TypeSwitch( - [&](const WaveTrack *wc) { - uNewTrack = wc->EmptyCopy( pSampleBlockFactory ); - pNewTrack = uNewTrack.get(); - }, -#ifdef USE_MIDI - [&](const NoteTrack *) { - uNewTrack = std::make_shared(), - pNewTrack = uNewTrack.get(); - }, -#endif - [&](const LabelTrack *) { - uNewTrack = std::make_shared(), - pNewTrack = uNewTrack.get(); - }, - [&](const TimeTrack *) { - // Maintain uniqueness of the time track! - pNewTrack = *tracks.Any().begin(); - if (!pNewTrack) - uNewTrack = std::make_shared( &viewInfo ), - pNewTrack = uNewTrack.get(); - } - ); - + auto pNewTrack = pClip->PasteInto( project ); + bool newTrack = (pNewTrack.use_count() == 1); wxASSERT(pClip); - pNewTrack->Paste(0.0, pClip); - if (!pFirstNewTrack) - pFirstNewTrack = pNewTrack; + pFirstNewTrack = pNewTrack.get(); pNewTrack->SetSelected(true); - if (uNewTrack) - FinishCopy(pClip, uNewTrack, tracks); + if (newTrack) + FinishCopy(pClip, pNewTrack, tracks); else - Track::FinishCopy(pClip, pNewTrack); + Track::FinishCopy(pClip, pNewTrack.get()); } // Select some pasted samples, which is probably impossible to get right @@ -263,9 +234,11 @@ void OnCut(const CommandContext &context) }, #endif [&](Track *n) { - auto dest = n->Copy(selectedRegion.t0(), - selectedRegion.t1()); - FinishCopy(n, dest, newClipboard); + if (n->SupportsBasicEditing()) { + auto dest = n->Copy(selectedRegion.t0(), + selectedRegion.t1()); + FinishCopy(n, dest, newClipboard); + } } ); @@ -298,8 +271,8 @@ void OnCut(const CommandContext &context) fallthrough(); }, [&](Track *n) { - n->Clear(selectedRegion.t0(), - selectedRegion.t1()); + if (n->SupportsBasicEditing()) + n->Clear(selectedRegion.t0(), selectedRegion.t1()); } ); @@ -321,6 +294,8 @@ void OnDelete(const CommandContext &context) auto &window = ProjectWindow::Get( project ); for (auto n : tracks.Any()) { + if (!n->SupportsBasicEditing()) + continue; if (n->GetSelected() || n->IsSyncLockSelected()) { n->Clear(selectedRegion.t0(), selectedRegion.t1()); } @@ -359,9 +334,11 @@ void OnCopy(const CommandContext &context) auto &newClipboard = *pNewClipboard; for (auto n : tracks.Selected()) { - auto dest = n->Copy(selectedRegion.t0(), - selectedRegion.t1()); - FinishCopy(n, dest, newClipboard); + if (n->SupportsBasicEditing()) { + auto dest = n->Copy(selectedRegion.t0(), + selectedRegion.t1()); + FinishCopy(n, dest, newClipboard); + } } // Survived possibility of exceptions. Commit changes to the clipboard now. @@ -643,6 +620,9 @@ void OnDuplicate(const CommandContext &context) auto range = tracks.Selected(); auto last = *range.rbegin(); for (auto n : range) { + if (!n->SupportsBasicEditing()) + continue; + // Make copies not for clipboard but for direct addition to the project auto dest = n->Copy(selectedRegion.t0(), selectedRegion.t1(), false); @@ -683,12 +663,14 @@ void OnSplitCut(const CommandContext &context) FinishCopy(n, dest, newClipboard); }, [&](Track *n) { - dest = n->Copy(selectedRegion.t0(), - selectedRegion.t1()); - n->Silence(selectedRegion.t0(), + if (n->SupportsBasicEditing()) { + dest = n->Copy(selectedRegion.t0(), selectedRegion.t1()); - if (dest) - FinishCopy(n, dest, newClipboard); + n->Silence(selectedRegion.t0(), + selectedRegion.t1()); + if (dest) + FinishCopy(n, dest, newClipboard); + } } ); @@ -713,8 +695,9 @@ void OnSplitDelete(const CommandContext &context) selectedRegion.t1()); }, [&](Track *n) { - n->Silence(selectedRegion.t0(), - selectedRegion.t1()); + if (n->SupportsBasicEditing()) + n->Silence(selectedRegion.t0(), + selectedRegion.t1()); } ); @@ -1004,7 +987,7 @@ static const ReservedCommandFlag if ( TimeSelectedPred( project ) && - TracksSelectedPred( project ) + EditableTracksSelectedPred( project ) ) return true; @@ -1020,7 +1003,7 @@ BaseItemSharedPtr EditMenu() using Options = CommandManager::Options; static const auto NotBusyTimeAndTracksFlags = - AudioIONotBusyFlag() | TimeSelectedFlag() | TracksSelectedFlag(); + AudioIONotBusyFlag() | TimeSelectedFlag() | EditableTracksSelectedFlag(); // The default shortcut key for Redo is different on different platforms. static constexpr auto redoKey = @@ -1065,7 +1048,7 @@ BaseItemSharedPtr EditMenu() AudioIONotBusyFlag() | CutCopyAvailableFlag() | NoAutoSelect(), wxT("Ctrl+X") ), Command( wxT("Delete"), XXO("&Delete"), FN(OnDelete), - AudioIONotBusyFlag() | TracksSelectedFlag() | TimeSelectedFlag() | NoAutoSelect(), + AudioIONotBusyFlag() | EditableTracksSelectedFlag() | TimeSelectedFlag() | NoAutoSelect(), wxT("Ctrl+K") ), /* i18n-hint: (verb)*/ Command( wxT("Copy"), XXO("&Copy"), FN(OnCopy), @@ -1160,7 +1143,7 @@ BaseItemSharedPtr ExtraEditMenu() { using Options = CommandManager::Options; static const auto flags = - AudioIONotBusyFlag() | TracksSelectedFlag() | TimeSelectedFlag(); + AudioIONotBusyFlag() | EditableTracksSelectedFlag() | TimeSelectedFlag(); static BaseItemSharedPtr menu{ ( FinderScope{ findCommandHandler }, Menu( wxT("Edit"), XXO("&Edit"), @@ -1184,7 +1167,7 @@ auto selectAll = []( AudacityProject &project, CommandFlag flagsRqd ){ RegisteredMenuItemEnabler selectTracks{{ []{ return TracksExistFlag(); }, - []{ return TracksSelectedFlag(); }, + []{ return EditableTracksSelectedFlag(); }, canSelectAll, selectAll }}; diff --git a/src/menus/SelectMenus.cpp b/src/menus/SelectMenus.cpp index e96068bb2..1a2bd0e66 100644 --- a/src/menus/SelectMenus.cpp +++ b/src/menus/SelectMenus.cpp @@ -483,7 +483,7 @@ void OnSelectSyncLockSel(const CommandContext &context) auto &tracks = TrackList::Get( project ); bool selected = false; - for (auto t : tracks.Any() + for (auto t : tracks.Any() + &Track::SupportsBasicEditing + &Track::IsSyncLockSelected - &Track::IsSelected) { t->SetSelected(true); selected = true; @@ -866,7 +866,7 @@ void OnCursorTrackStart(const CommandContext &context) double kWayOverToRight = std::numeric_limits::max(); - auto trackRange = tracks.Selected(); + auto trackRange = tracks.Selected() + &Track::SupportsBasicEditing; if (trackRange.empty()) // This should have been prevented by command manager return; @@ -892,7 +892,7 @@ void OnCursorTrackEnd(const CommandContext &context) double kWayOverToLeft = std::numeric_limits::lowest(); - auto trackRange = tracks.Selected(); + auto trackRange = tracks.Selected() + &Track::SupportsBasicEditing; if (trackRange.empty()) // This should have been prevented by command manager return; @@ -1059,7 +1059,7 @@ BaseItemSharedPtr SelectMenu() , Command( wxT("SelSyncLockTracks"), XXO("In All &Sync-Locked Tracks"), FN(OnSelectSyncLockSel), - TracksSelectedFlag() | IsSyncLockedFlag(), + EditableTracksSelectedFlag() | IsSyncLockedFlag(), Options{ wxT("Ctrl+Shift+Y"), XO("Select Sync-Locked") } ) #endif ), @@ -1131,7 +1131,7 @@ BaseItemSharedPtr SelectMenu() Section( "", Command( wxT("ZeroCross"), XXO("At &Zero Crossings"), - FN(OnZeroCrossing), TracksSelectedFlag(), + FN(OnZeroCrossing), EditableTracksSelectedFlag(), Options{ wxT("Z"), XO("Select Zero Crossing") } ) ) ) ) }; @@ -1216,11 +1216,11 @@ BaseItemSharedPtr CursorMenu() Command( wxT("CursTrackStart"), XXO("Track &Start"), FN(OnCursorTrackStart), - TracksSelectedFlag(), + EditableTracksSelectedFlag(), Options{ wxT("J"), XO("Cursor to Track Start") } ), Command( wxT("CursTrackEnd"), XXO("Track &End"), FN(OnCursorTrackEnd), - TracksSelectedFlag(), + EditableTracksSelectedFlag(), Options{ wxT("K"), XO("Cursor to Track End") } ), Command( wxT("CursProjectStart"), XXO("&Project Start"), diff --git a/src/menus/TrackMenus.cpp b/src/menus/TrackMenus.cpp index ab8fc7c52..0abe11d1b 100644 --- a/src/menus/TrackMenus.cpp +++ b/src/menus/TrackMenus.cpp @@ -1356,9 +1356,9 @@ BaseItemSharedPtr TracksMenu() Command( wxT("UnmuteAllTracks"), XXO("&Unmute All Tracks"), FN(OnUnmuteAllTracks), TracksExistFlag(), wxT("Ctrl+Shift+U") ), Command( wxT("MuteTracks"), XXO("Mut&e Tracks"), - FN(OnMuteSelectedTracks), TracksSelectedFlag(), wxT("Ctrl+Alt+U") ), + FN(OnMuteSelectedTracks), EditableTracksSelectedFlag(), wxT("Ctrl+Alt+U") ), Command( wxT("UnmuteTracks"), XXO("U&nmute Tracks"), - FN(OnUnmuteSelectedTracks), TracksSelectedFlag(), wxT("Ctrl+Alt+Shift+U") ) + FN(OnUnmuteSelectedTracks), EditableTracksSelectedFlag(), wxT("Ctrl+Alt+Shift+U") ) ), Menu( wxT("Pan"), XXO("&Pan"), @@ -1367,13 +1367,13 @@ BaseItemSharedPtr TracksMenu() // in the project could very easily be lost unless we // require the tracks to be selected. Command( wxT("PanLeft"), XXO("&Left"), FN(OnPanLeft), - TracksSelectedFlag(), + EditableTracksSelectedFlag(), Options{}.LongName( XO("Pan Left") ) ), Command( wxT("PanRight"), XXO("&Right"), FN(OnPanRight), - TracksSelectedFlag(), + EditableTracksSelectedFlag(), Options{}.LongName( XO("Pan Right") ) ), Command( wxT("PanCenter"), XXO("&Center"), FN(OnPanCenter), - TracksSelectedFlag(), + EditableTracksSelectedFlag(), Options{}.LongName( XO("Pan Center") ) ) ) ), @@ -1387,14 +1387,14 @@ BaseItemSharedPtr TracksMenu() { wxT("EndToEnd"), XXO("&Align End to End") }, { wxT("Together"), XXO("Align &Together") }, }, - FN(OnAlignNoSync), AudioIONotBusyFlag() | TracksSelectedFlag()) + FN(OnAlignNoSync), AudioIONotBusyFlag() | EditableTracksSelectedFlag()) ), Section( "", // Alignment commands using selection or zero CommandGroup(wxT("Align"), alignLabels(), - FN(OnAlign), AudioIONotBusyFlag() | TracksSelectedFlag()) + FN(OnAlign), AudioIONotBusyFlag() | EditableTracksSelectedFlag()) ), Section( "", @@ -1411,7 +1411,7 @@ BaseItemSharedPtr TracksMenu() // Do we need this sub-menu at all? Menu( wxT("MoveSelectionAndTracks"), XO("Move Sele&ction and Tracks"), { CommandGroup(wxT("AlignMove"), alignLabels(), - FN(OnAlignMoveSel), AudioIONotBusyFlag() | TracksSelectedFlag()), + FN(OnAlignMoveSel), AudioIONotBusyFlag() | EditableTracksSelectedFlag()), } ), #endif