mirror of
https://github.com/cookiengineer/audacity
synced 2025-06-16 08:09:32 +02:00
Redo aup3 import more simply; fix cut, copy, import of TimeTrack
This commit is contained in:
commit
eb888bb03b
@ -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<const TimeTrack*>( 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 );
|
||||
|
@ -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
|
||||
|
||||
|
@ -85,6 +85,13 @@ LabelTrack::LabelTrack(const LabelTrack &orig) :
|
||||
}
|
||||
}
|
||||
|
||||
Track::Holder LabelTrack::PasteInto( AudacityProject & ) const
|
||||
{
|
||||
auto pNewTrack = std::make_shared<LabelTrack>();
|
||||
pNewTrack->Paste(0.0, this);
|
||||
return pNewTrack;
|
||||
}
|
||||
|
||||
template<typename IntervalType>
|
||||
static IntervalType DoMakeInterval(const LabelStruct &label, size_t index)
|
||||
{
|
||||
|
@ -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} {};
|
||||
|
@ -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<NoteTrack>();
|
||||
pNewTrack->Paste(0.0, this);
|
||||
return pNewTrack;
|
||||
}
|
||||
|
||||
auto NoteTrack::GetIntervals() const -> ConstIntervals
|
||||
{
|
||||
ConstIntervals results;
|
||||
|
@ -185,6 +185,8 @@ public:
|
||||
mVisibleChannels = CHANNEL_BIT(c);
|
||||
}
|
||||
|
||||
Track::Holder PasteInto( AudacityProject & ) const override;
|
||||
|
||||
ConstIntervals GetIntervals() const override;
|
||||
Intervals GetIntervals() override;
|
||||
|
||||
|
@ -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<wxXmlNode *> blocknodes;
|
||||
std::function<void(wxXmlNode *)> 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<TimeTrack>().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;
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -268,6 +268,13 @@ std::shared_ptr<Tags> Tags::Duplicate() const
|
||||
return std::make_shared<Tags>(*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;
|
||||
|
@ -88,6 +88,8 @@ class AUDACITY_DLL_API Tags final
|
||||
|
||||
std::shared_ptr<Tags> Duplicate() const;
|
||||
|
||||
void Merge( const Tags &other );
|
||||
|
||||
Tags & operator= (const Tags & src );
|
||||
|
||||
bool ShowEditDialog(
|
||||
|
@ -52,6 +52,11 @@ static ProjectFileIORegistry::Entry registerFactory{
|
||||
TimeTrack::TimeTrack(const ZoomInfo *zoomInfo):
|
||||
Track()
|
||||
, mZoomInfo(zoomInfo)
|
||||
{
|
||||
CleanState();
|
||||
}
|
||||
|
||||
void TimeTrack::CleanState()
|
||||
{
|
||||
mEnvelope = std::make_unique<BoundedEnvelope>(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<TimeTrack> pNewTrack;
|
||||
if( auto pTrack = *TrackList::Get( project ).Any<TimeTrack>().begin() )
|
||||
pNewTrack = pTrack->SharedPointer<TimeTrack>();
|
||||
else
|
||||
pNewTrack = std::make_shared<TimeTrack>( &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 );
|
||||
|
@ -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; }
|
||||
|
||||
|
@ -1224,6 +1224,11 @@ std::shared_ptr<const Track> Track::SubstituteOriginalTrack() const
|
||||
return SharedPointer();
|
||||
}
|
||||
|
||||
bool Track::SupportsBasicEditing() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
auto Track::GetIntervals() const -> ConstIntervals
|
||||
{
|
||||
return {};
|
||||
|
10
src/Track.h
10
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<Track>;
|
||||
|
||||
//! 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<Track>;
|
||||
// public nonvirtual duplication function that invokes Clone():
|
||||
virtual Holder Duplicate() const;
|
||||
|
||||
|
@ -334,6 +334,15 @@ static Container MakeIntervals(const std::vector<WaveClipHolder> &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<ConstIntervals>( mClips );
|
||||
|
@ -537,6 +537,8 @@ private:
|
||||
std::shared_ptr<WaveClip> pClip;
|
||||
};
|
||||
|
||||
Track::Holder PasteInto( AudacityProject & ) const override;
|
||||
|
||||
ConstIntervals GetIntervals() const override;
|
||||
Intervals GetIntervals() override;
|
||||
|
||||
|
@ -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<NoteTrack>(),
|
||||
pNewTrack = uNewTrack.get();
|
||||
},
|
||||
#endif
|
||||
[&](const LabelTrack *) {
|
||||
uNewTrack = std::make_shared<LabelTrack>(),
|
||||
pNewTrack = uNewTrack.get();
|
||||
},
|
||||
[&](const TimeTrack *) {
|
||||
// Maintain uniqueness of the time track!
|
||||
pNewTrack = *tracks.Any<TimeTrack>().begin();
|
||||
if (!pNewTrack)
|
||||
uNewTrack = std::make_shared<TimeTrack>( &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
|
||||
}};
|
||||
|
@ -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<double>::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<double>::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"),
|
||||
|
@ -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
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user