mirror of
https://github.com/cookiengineer/audacity
synced 2025-07-14 15:48:21 +02:00
!!! THERE WILL NO DOUBT BE BUGS !!! This is a big one and there's still several things to complete. Just want to get this in the wild to start receiving feedback. One big thing right now is that it will NOT load pre-aup3 files. An importer is on the way for that.
1031 lines
33 KiB
C++
1031 lines
33 KiB
C++
/**********************************************************************
|
|
|
|
Audacity: A Digital Audio Editor
|
|
|
|
ProjectManager.cpp
|
|
|
|
Paul Licameli split from AudacityProject.cpp
|
|
|
|
**********************************************************************/
|
|
|
|
#include "ProjectManager.h"
|
|
|
|
#include "Experimental.h"
|
|
|
|
#include "AdornedRulerPanel.h"
|
|
#include "AudioIO.h"
|
|
#include "AutoRecovery.h"
|
|
#include "Clipboard.h"
|
|
#include "FileNames.h"
|
|
#include "Menus.h"
|
|
#include "ModuleManager.h"
|
|
#include "Project.h"
|
|
#include "ProjectAudioIO.h"
|
|
#include "ProjectAudioManager.h"
|
|
#include "ProjectFileIO.h"
|
|
#include "ProjectFileManager.h"
|
|
#include "ProjectHistory.h"
|
|
#include "ProjectSelectionManager.h"
|
|
#include "ProjectSettings.h"
|
|
#include "ProjectStatus.h"
|
|
#include "ProjectWindow.h"
|
|
#include "SelectUtilities.h"
|
|
#include "TrackPanel.h"
|
|
#include "TrackUtilities.h"
|
|
#include "UndoManager.h"
|
|
#include "WaveTrack.h"
|
|
#include "wxFileNameWrapper.h"
|
|
#include "import/Import.h"
|
|
#include "import/ImportMIDI.h"
|
|
#include "prefs/QualityPrefs.h"
|
|
#include "toolbars/MixerToolBar.h"
|
|
#include "toolbars/SelectionBar.h"
|
|
#include "toolbars/SpectralSelectionBar.h"
|
|
#include "toolbars/TimeToolBar.h"
|
|
#include "toolbars/ToolManager.h"
|
|
#include "widgets/AudacityMessageBox.h"
|
|
#include "widgets/FileHistory.h"
|
|
#include "widgets/ErrorDialog.h"
|
|
#include "widgets/WindowAccessible.h"
|
|
|
|
#include <wx/dataobj.h>
|
|
#include <wx/dnd.h>
|
|
#include <wx/scrolbar.h>
|
|
#include <wx/sizer.h>
|
|
|
|
#ifdef __WXGTK__
|
|
#include "../images/AudacityLogoAlpha.xpm"
|
|
#endif
|
|
|
|
const int AudacityProjectTimerID = 5200;
|
|
|
|
static AudacityProject::AttachedObjects::RegisteredFactory sProjectManagerKey {
|
|
[]( AudacityProject &project ) {
|
|
return std::make_shared< ProjectManager >( project );
|
|
}
|
|
};
|
|
|
|
ProjectManager &ProjectManager::Get( AudacityProject &project )
|
|
{
|
|
return project.AttachedObjects::Get< ProjectManager >( sProjectManagerKey );
|
|
}
|
|
|
|
const ProjectManager &ProjectManager::Get( const AudacityProject &project )
|
|
{
|
|
return Get( const_cast< AudacityProject & >( project ) );
|
|
}
|
|
|
|
ProjectManager::ProjectManager( AudacityProject &project )
|
|
: mProject{ project }
|
|
, mTimer{ std::make_unique<wxTimer>(this, AudacityProjectTimerID) }
|
|
{
|
|
auto &window = ProjectWindow::Get( mProject );
|
|
window.Bind( wxEVT_CLOSE_WINDOW, &ProjectManager::OnCloseWindow, this );
|
|
mProject.Bind(EVT_PROJECT_STATUS_UPDATE,
|
|
&ProjectManager::OnStatusChange, this);
|
|
}
|
|
|
|
ProjectManager::~ProjectManager() = default;
|
|
|
|
// PRL: This event type definition used to be in AudacityApp.h, which created
|
|
// a bad compilation dependency. The event was never emitted anywhere. I
|
|
// preserve it and its handler here but I move it to remove the dependency.
|
|
// Asynchronous open
|
|
wxDECLARE_EXPORTED_EVENT(AUDACITY_DLL_API,
|
|
EVT_OPEN_AUDIO_FILE, wxCommandEvent);
|
|
wxDEFINE_EVENT(EVT_OPEN_AUDIO_FILE, wxCommandEvent);
|
|
|
|
BEGIN_EVENT_TABLE( ProjectManager, wxEvtHandler )
|
|
EVT_COMMAND(wxID_ANY, EVT_OPEN_AUDIO_FILE, ProjectManager::OnOpenAudioFile)
|
|
EVT_TIMER(AudacityProjectTimerID, ProjectManager::OnTimer)
|
|
END_EVENT_TABLE()
|
|
|
|
bool ProjectManager::sbWindowRectAlreadySaved = false;
|
|
bool ProjectManager::sbSkipPromptingForSave = false;
|
|
|
|
void ProjectManager::SaveWindowSize()
|
|
{
|
|
if (sbWindowRectAlreadySaved)
|
|
{
|
|
return;
|
|
}
|
|
bool validWindowForSaveWindowSize = FALSE;
|
|
ProjectWindow * validProject = nullptr;
|
|
bool foundIconizedProject = FALSE;
|
|
for ( auto pProject : AllProjects{} )
|
|
{
|
|
auto &window = ProjectWindow::Get( *pProject );
|
|
if (!window.IsIconized()) {
|
|
validWindowForSaveWindowSize = TRUE;
|
|
validProject = &window;
|
|
break;
|
|
}
|
|
else
|
|
foundIconizedProject = TRUE;
|
|
|
|
}
|
|
if (validWindowForSaveWindowSize)
|
|
{
|
|
wxRect windowRect = validProject->GetRect();
|
|
wxRect normalRect = validProject->GetNormalizedWindowState();
|
|
bool wndMaximized = validProject->IsMaximized();
|
|
gPrefs->Write(wxT("/Window/X"), windowRect.GetX());
|
|
gPrefs->Write(wxT("/Window/Y"), windowRect.GetY());
|
|
gPrefs->Write(wxT("/Window/Width"), windowRect.GetWidth());
|
|
gPrefs->Write(wxT("/Window/Height"), windowRect.GetHeight());
|
|
gPrefs->Write(wxT("/Window/Maximized"), wndMaximized);
|
|
gPrefs->Write(wxT("/Window/Normal_X"), normalRect.GetX());
|
|
gPrefs->Write(wxT("/Window/Normal_Y"), normalRect.GetY());
|
|
gPrefs->Write(wxT("/Window/Normal_Width"), normalRect.GetWidth());
|
|
gPrefs->Write(wxT("/Window/Normal_Height"), normalRect.GetHeight());
|
|
gPrefs->Write(wxT("/Window/Iconized"), FALSE);
|
|
}
|
|
else
|
|
{
|
|
if (foundIconizedProject) {
|
|
validProject = &ProjectWindow::Get( **AllProjects{}.begin() );
|
|
bool wndMaximized = validProject->IsMaximized();
|
|
wxRect normalRect = validProject->GetNormalizedWindowState();
|
|
// store only the normal rectangle because the itemized rectangle
|
|
// makes no sense for an opening project window
|
|
gPrefs->Write(wxT("/Window/X"), normalRect.GetX());
|
|
gPrefs->Write(wxT("/Window/Y"), normalRect.GetY());
|
|
gPrefs->Write(wxT("/Window/Width"), normalRect.GetWidth());
|
|
gPrefs->Write(wxT("/Window/Height"), normalRect.GetHeight());
|
|
gPrefs->Write(wxT("/Window/Maximized"), wndMaximized);
|
|
gPrefs->Write(wxT("/Window/Normal_X"), normalRect.GetX());
|
|
gPrefs->Write(wxT("/Window/Normal_Y"), normalRect.GetY());
|
|
gPrefs->Write(wxT("/Window/Normal_Width"), normalRect.GetWidth());
|
|
gPrefs->Write(wxT("/Window/Normal_Height"), normalRect.GetHeight());
|
|
gPrefs->Write(wxT("/Window/Iconized"), TRUE);
|
|
}
|
|
else {
|
|
// this would be a very strange case that might possibly occur on the Mac
|
|
// Audacity would have to be running with no projects open
|
|
// in this case we are going to write only the default values
|
|
wxRect defWndRect;
|
|
GetDefaultWindowRect(&defWndRect);
|
|
gPrefs->Write(wxT("/Window/X"), defWndRect.GetX());
|
|
gPrefs->Write(wxT("/Window/Y"), defWndRect.GetY());
|
|
gPrefs->Write(wxT("/Window/Width"), defWndRect.GetWidth());
|
|
gPrefs->Write(wxT("/Window/Height"), defWndRect.GetHeight());
|
|
gPrefs->Write(wxT("/Window/Maximized"), FALSE);
|
|
gPrefs->Write(wxT("/Window/Normal_X"), defWndRect.GetX());
|
|
gPrefs->Write(wxT("/Window/Normal_Y"), defWndRect.GetY());
|
|
gPrefs->Write(wxT("/Window/Normal_Width"), defWndRect.GetWidth());
|
|
gPrefs->Write(wxT("/Window/Normal_Height"), defWndRect.GetHeight());
|
|
gPrefs->Write(wxT("/Window/Iconized"), FALSE);
|
|
}
|
|
}
|
|
gPrefs->Flush();
|
|
sbWindowRectAlreadySaved = true;
|
|
}
|
|
|
|
#if wxUSE_DRAG_AND_DROP
|
|
class FileObject final : public wxFileDataObject
|
|
{
|
|
public:
|
|
FileObject()
|
|
{
|
|
}
|
|
|
|
bool IsSupportedFormat(const wxDataFormat & format, Direction WXUNUSED(dir = Get)) const
|
|
// PRL: This function does NOT override any inherited virtual! What does it do?
|
|
{
|
|
if (format.GetType() == wxDF_FILENAME) {
|
|
return true;
|
|
}
|
|
|
|
#if defined(__WXMAC__)
|
|
#if !wxCHECK_VERSION(3, 0, 0)
|
|
if (format.GetFormatId() == kDragPromisedFlavorFindFile) {
|
|
return true;
|
|
}
|
|
#endif
|
|
#endif
|
|
|
|
return false;
|
|
}
|
|
};
|
|
|
|
class DropTarget final : public wxFileDropTarget
|
|
{
|
|
public:
|
|
DropTarget(AudacityProject *proj)
|
|
{
|
|
mProject = proj;
|
|
|
|
// SetDataObject takes ownership
|
|
SetDataObject(safenew FileObject());
|
|
}
|
|
|
|
~DropTarget()
|
|
{
|
|
}
|
|
|
|
#if defined(__WXMAC__)
|
|
#if !wxCHECK_VERSION(3, 0, 0)
|
|
bool GetData() override
|
|
{
|
|
bool foundSupported = false;
|
|
bool firstFileAdded = false;
|
|
OSErr result;
|
|
|
|
UInt16 items = 0;
|
|
CountDragItems((DragReference)m_currentDrag, &items);
|
|
|
|
for (UInt16 index = 1; index <= items; index++) {
|
|
|
|
DragItemRef theItem = 0;
|
|
GetDragItemReferenceNumber((DragReference)m_currentDrag, index, &theItem);
|
|
|
|
UInt16 flavors = 0;
|
|
CountDragItemFlavors((DragReference)m_currentDrag, theItem , &flavors ) ;
|
|
|
|
for (UInt16 flavor = 1 ;flavor <= flavors; flavor++) {
|
|
|
|
FlavorType theType = 0;
|
|
result = GetFlavorType((DragReference)m_currentDrag, theItem, flavor, &theType);
|
|
if (theType != kDragPromisedFlavorFindFile && theType != kDragFlavorTypeHFS) {
|
|
continue;
|
|
}
|
|
foundSupported = true;
|
|
|
|
Size dataSize = 0;
|
|
GetFlavorDataSize((DragReference)m_currentDrag, theItem, theType, &dataSize);
|
|
|
|
ArrayOf<char> theData{ dataSize };
|
|
GetFlavorData((DragReference)m_currentDrag, theItem, theType, (void*) theData.get(), &dataSize, 0L);
|
|
|
|
wxString name;
|
|
if (theType == kDragPromisedFlavorFindFile) {
|
|
name = wxMacFSSpec2MacFilename((FSSpec *)theData.get());
|
|
}
|
|
else if (theType == kDragFlavorTypeHFS) {
|
|
name = wxMacFSSpec2MacFilename(&((HFSFlavor *)theData.get())->fileSpec);
|
|
}
|
|
|
|
if (!firstFileAdded) {
|
|
// reset file list
|
|
((wxFileDataObject*)GetDataObject())->SetData(0, "");
|
|
firstFileAdded = true;
|
|
}
|
|
|
|
((wxFileDataObject*)GetDataObject())->AddFile(name);
|
|
|
|
// We only want to process one flavor
|
|
break;
|
|
}
|
|
}
|
|
return foundSupported;
|
|
}
|
|
#endif
|
|
|
|
bool OnDrop(wxCoord x, wxCoord y) override
|
|
{
|
|
// bool foundSupported = false;
|
|
#if !wxCHECK_VERSION(3, 0, 0)
|
|
bool firstFileAdded = false;
|
|
OSErr result;
|
|
|
|
UInt16 items = 0;
|
|
CountDragItems((DragReference)m_currentDrag, &items);
|
|
|
|
for (UInt16 index = 1; index <= items; index++) {
|
|
|
|
DragItemRef theItem = 0;
|
|
GetDragItemReferenceNumber((DragReference)m_currentDrag, index, &theItem);
|
|
|
|
UInt16 flavors = 0;
|
|
CountDragItemFlavors((DragReference)m_currentDrag, theItem , &flavors ) ;
|
|
|
|
for (UInt16 flavor = 1 ;flavor <= flavors; flavor++) {
|
|
|
|
FlavorType theType = 0;
|
|
result = GetFlavorType((DragReference)m_currentDrag, theItem, flavor, &theType);
|
|
if (theType != kDragPromisedFlavorFindFile && theType != kDragFlavorTypeHFS) {
|
|
continue;
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
#endif
|
|
return CurrentDragHasSupportedFormat();
|
|
}
|
|
|
|
#endif
|
|
|
|
bool OnDropFiles(wxCoord WXUNUSED(x), wxCoord WXUNUSED(y), const wxArrayString& filenames) override
|
|
{
|
|
// Experiment shows that this function can be reached while there is no
|
|
// catch block above in wxWidgets. So stop all exceptions here.
|
|
return GuardedCall< bool > ( [&] {
|
|
wxArrayString sortednames(filenames);
|
|
sortednames.Sort(FileNames::CompareNoCase);
|
|
|
|
auto cleanup = finally( [&] {
|
|
ProjectWindow::Get( *mProject ).HandleResize(); // Adjust scrollers for NEW track sizes.
|
|
} );
|
|
|
|
for (const auto &name : sortednames) {
|
|
#ifdef USE_MIDI
|
|
if (FileNames::IsMidi(name))
|
|
DoImportMIDI( *mProject, name );
|
|
else
|
|
#endif
|
|
ProjectFileManager::Get( *mProject ).Import(name);
|
|
}
|
|
|
|
auto &window = ProjectWindow::Get( *mProject );
|
|
window.ZoomAfterImport(nullptr);
|
|
|
|
return true;
|
|
} );
|
|
}
|
|
|
|
private:
|
|
AudacityProject *mProject;
|
|
};
|
|
|
|
#endif
|
|
|
|
void InitProjectWindow( ProjectWindow &window )
|
|
{
|
|
auto &project = window.GetProject();
|
|
|
|
#ifdef EXPERIMENTAL_DA2
|
|
SetBackgroundColour(theTheme.Colour( clrMedium ));
|
|
#endif
|
|
// Note that the first field of the status bar is a dummy, and its width is set
|
|
// to zero latter in the code. This field is needed for wxWidgets 2.8.12 because
|
|
// if you move to the menu bar, the first field of the menu bar is cleared, which
|
|
// is undesirable behaviour.
|
|
// In addition, the help strings of menu items are by default sent to the first
|
|
// field. Currently there are no such help strings, but it they were introduced, then
|
|
// there would need to be an event handler to send them to the appropriate field.
|
|
auto statusBar = window.CreateStatusBar(4);
|
|
#if wxUSE_ACCESSIBILITY
|
|
// so that name can be set on a standard control
|
|
statusBar->SetAccessible(safenew WindowAccessible(statusBar));
|
|
#endif
|
|
statusBar->SetName(wxT("status_line")); // not localized
|
|
|
|
auto &viewInfo = ViewInfo::Get( project );
|
|
|
|
// LLL: Read this!!!
|
|
//
|
|
// Until the time (and cpu) required to refresh the track panel is
|
|
// reduced, leave the following window creations in the order specified.
|
|
// This will place the refresh of the track panel last, allowing all
|
|
// the others to get done quickly.
|
|
//
|
|
// Near as I can tell, this is only a problem under Windows.
|
|
//
|
|
|
|
|
|
//
|
|
// Create the ToolDock
|
|
//
|
|
ToolManager::Get( project ).CreateWindows();
|
|
ToolManager::Get( project ).LayoutToolBars();
|
|
|
|
//
|
|
// Create the horizontal ruler
|
|
//
|
|
auto &ruler = AdornedRulerPanel::Get( project );
|
|
|
|
//
|
|
// Create the TrackPanel and the scrollbars
|
|
//
|
|
|
|
auto topPanel = window.GetTopPanel();
|
|
|
|
{
|
|
auto ubs = std::make_unique<wxBoxSizer>(wxVERTICAL);
|
|
ubs->Add( ToolManager::Get( project ).GetTopDock(), 0, wxEXPAND | wxALIGN_TOP );
|
|
ubs->Add(&ruler, 0, wxEXPAND);
|
|
topPanel->SetSizer(ubs.release());
|
|
}
|
|
|
|
// Ensure that the topdock comes before the ruler in the tab order,
|
|
// irrespective of the order in which they were created.
|
|
ToolManager::Get(project).GetTopDock()->MoveBeforeInTabOrder(&ruler);
|
|
|
|
const auto pPage = window.GetMainPage();
|
|
|
|
wxBoxSizer *bs;
|
|
{
|
|
auto ubs = std::make_unique<wxBoxSizer>(wxVERTICAL);
|
|
bs = ubs.get();
|
|
bs->Add(topPanel, 0, wxEXPAND | wxALIGN_TOP);
|
|
bs->Add(pPage, 1, wxEXPAND);
|
|
bs->Add( ToolManager::Get( project ).GetBotDock(), 0, wxEXPAND );
|
|
window.SetAutoLayout(true);
|
|
window.SetSizer(ubs.release());
|
|
}
|
|
bs->Layout();
|
|
|
|
auto &trackPanel = TrackPanel::Get( project );
|
|
|
|
// LLL: When Audacity starts or becomes active after returning from
|
|
// another application, the first window that can accept focus
|
|
// will be given the focus even if we try to SetFocus(). By
|
|
// making the TrackPanel that first window, we resolve several
|
|
// keyboard focus problems.
|
|
pPage->MoveBeforeInTabOrder(topPanel);
|
|
|
|
bs = (wxBoxSizer *)pPage->GetSizer();
|
|
|
|
auto vsBar = &window.GetVerticalScrollBar();
|
|
auto hsBar = &window.GetHorizontalScrollBar();
|
|
|
|
{
|
|
// Top horizontal grouping
|
|
auto hs = std::make_unique<wxBoxSizer>(wxHORIZONTAL);
|
|
|
|
// Track panel
|
|
hs->Add(&trackPanel, 1, wxEXPAND | wxALIGN_LEFT | wxALIGN_TOP);
|
|
|
|
{
|
|
// Vertical grouping
|
|
auto vs = std::make_unique<wxBoxSizer>(wxVERTICAL);
|
|
|
|
// Vertical scroll bar
|
|
vs->Add(vsBar, 1, wxEXPAND | wxALIGN_TOP);
|
|
hs->Add(vs.release(), 0, wxEXPAND | wxALIGN_TOP);
|
|
}
|
|
|
|
bs->Add(hs.release(), 1, wxEXPAND | wxALIGN_LEFT | wxALIGN_TOP);
|
|
}
|
|
|
|
{
|
|
// Bottom horizontal grouping
|
|
auto hs = std::make_unique<wxBoxSizer>(wxHORIZONTAL);
|
|
|
|
// Bottom scrollbar
|
|
hs->Add(viewInfo.GetLeftOffset() - 1, 0);
|
|
hs->Add(hsBar, 1, wxALIGN_BOTTOM);
|
|
hs->Add(vsBar->GetSize().GetWidth(), 0);
|
|
bs->Add(hs.release(), 0, wxEXPAND | wxALIGN_LEFT);
|
|
}
|
|
|
|
// Lay it out
|
|
pPage->SetAutoLayout(true);
|
|
pPage->Layout();
|
|
|
|
#ifdef EXPERIMENTAL_NOTEBOOK
|
|
AddPages(this, Factory, pNotebook);
|
|
#endif
|
|
|
|
auto mainPanel = window.GetMainPanel();
|
|
|
|
mainPanel->Layout();
|
|
|
|
wxASSERT( trackPanel.GetProject() == &project );
|
|
|
|
// MM: Give track panel the focus to ensure keyboard commands work
|
|
trackPanel.SetFocus();
|
|
|
|
window.FixScrollbars();
|
|
ruler.SetLeftOffset(viewInfo.GetLeftOffset()); // bevel on AdornedRuler
|
|
|
|
//
|
|
// Set the Icon
|
|
//
|
|
|
|
// loads either the XPM or the windows resource, depending on the platform
|
|
#if !defined(__WXMAC__) && !defined(__WXX11__)
|
|
{
|
|
#if defined(__WXMSW__)
|
|
wxIcon ic{ wxICON(AudacityLogo) };
|
|
#elif defined(__WXGTK__)
|
|
wxIcon ic{wxICON(AudacityLogoAlpha)};
|
|
#else
|
|
wxIcon ic{};
|
|
ic.CopyFromBitmap(theTheme.Bitmap(bmpAudacityLogo48x48));
|
|
#endif
|
|
window.SetIcon(ic);
|
|
}
|
|
#endif
|
|
|
|
window.UpdateStatusWidths();
|
|
auto msg = XO("Welcome to Audacity version %s")
|
|
.Format( AUDACITY_VERSION_STRING );
|
|
ProjectManager::Get( project ).SetStatusText( msg, mainStatusBarField );
|
|
|
|
#ifdef EXPERIMENTAL_DA2
|
|
ClearBackground();// For wxGTK.
|
|
#endif
|
|
}
|
|
|
|
AudacityProject *ProjectManager::New()
|
|
{
|
|
wxRect wndRect;
|
|
bool bMaximized = false;
|
|
bool bIconized = false;
|
|
GetNextWindowPlacement(&wndRect, &bMaximized, &bIconized);
|
|
|
|
// Create and show a NEW project
|
|
// Use a non-default deleter in the smart pointer!
|
|
auto sp = std::make_shared< AudacityProject >();
|
|
AllProjects{}.Add( sp );
|
|
auto p = sp.get();
|
|
auto &project = *p;
|
|
auto &projectHistory = ProjectHistory::Get( project );
|
|
auto &projectManager = Get( project );
|
|
auto &window = ProjectWindow::Get( *p );
|
|
InitProjectWindow( window );
|
|
|
|
ProjectFileIO::Get( *p ).SetProjectTitle();
|
|
|
|
MenuManager::Get( project ).CreateMenusAndCommands( project );
|
|
|
|
projectHistory.InitialState();
|
|
projectManager.RestartTimer();
|
|
|
|
// wxGTK3 seems to need to require creating the window using default position
|
|
// and then manually positioning it.
|
|
window.SetPosition(wndRect.GetPosition());
|
|
|
|
if(bMaximized) {
|
|
window.Maximize(true);
|
|
}
|
|
else if (bIconized) {
|
|
// if the user close down and iconized state we could start back up and iconized state
|
|
// window.Iconize(TRUE);
|
|
}
|
|
|
|
//Initialise the Listeners
|
|
auto gAudioIO = AudioIO::Get();
|
|
gAudioIO->SetListener(
|
|
ProjectAudioManager::Get( project ).shared_from_this() );
|
|
auto &projectSelectionManager = ProjectSelectionManager::Get( project );
|
|
SelectionBar::Get( project ).SetListener( &projectSelectionManager );
|
|
#ifdef EXPERIMENTAL_SPECTRAL_EDITING
|
|
SpectralSelectionBar::Get( project ).SetListener( &projectSelectionManager );
|
|
#endif
|
|
TimeToolBar::Get( project ).SetListener( &projectSelectionManager );
|
|
|
|
#if wxUSE_DRAG_AND_DROP
|
|
// We can import now, so become a drag target
|
|
// SetDropTarget(safenew AudacityDropTarget(this));
|
|
// mTrackPanel->SetDropTarget(safenew AudacityDropTarget(this));
|
|
|
|
// SetDropTarget takes ownership
|
|
TrackPanel::Get( project ).SetDropTarget( safenew DropTarget( &project ) );
|
|
#endif
|
|
|
|
//Set the NEW project as active:
|
|
SetActiveProject(p);
|
|
|
|
// Okay, GetActiveProject() is ready. Now we can get its CommandManager,
|
|
// and add the shortcut keys to the tooltips.
|
|
ToolManager::Get( *p ).RegenerateTooltips();
|
|
|
|
ModuleManager::Get().Dispatch(ProjectInitialized);
|
|
|
|
window.Show(true);
|
|
|
|
return p;
|
|
}
|
|
|
|
void ProjectManager::OnCloseWindow(wxCloseEvent & event)
|
|
{
|
|
auto &project = mProject;
|
|
auto &projectFileIO = ProjectFileIO::Get( project );
|
|
auto &projectFileManager = ProjectFileManager::Get( project );
|
|
const auto &settings = ProjectSettings::Get( project );
|
|
auto &projectAudioIO = ProjectAudioIO::Get( project );
|
|
auto &tracks = TrackList::Get( project );
|
|
auto &window = ProjectWindow::Get( project );
|
|
auto gAudioIO = AudioIO::Get();
|
|
|
|
// We are called for the wxEVT_CLOSE_WINDOW, wxEVT_END_SESSION, and
|
|
// wxEVT_QUERY_END_SESSION, so we have to protect against multiple
|
|
// entries. This is a hack until the whole application termination
|
|
// process can be reviewed and reworked. (See bug #964 for ways
|
|
// to exercise the bug that instigated this hack.)
|
|
if (window.IsBeingDeleted())
|
|
{
|
|
event.Skip();
|
|
return;
|
|
}
|
|
|
|
if (event.CanVeto() && (::wxIsBusy() || project.mbBusyImporting))
|
|
{
|
|
event.Veto();
|
|
return;
|
|
}
|
|
|
|
// Check to see if we were playing or recording
|
|
// audio, and if so, make sure Audio I/O is completely finished.
|
|
// The main point of this is to properly push the state
|
|
// and flush the tracks once we've completely finished
|
|
// recording NEW state.
|
|
// This code is derived from similar code in
|
|
// AudacityProject::~AudacityProject() and TrackPanel::OnTimer().
|
|
if (projectAudioIO.GetAudioIOToken()>0 &&
|
|
gAudioIO->IsStreamActive(projectAudioIO.GetAudioIOToken())) {
|
|
|
|
// We were playing or recording audio, but we've stopped the stream.
|
|
ProjectAudioManager::Get( project ).Stop();
|
|
|
|
projectAudioIO.SetAudioIOToken(0);
|
|
window.RedrawProject();
|
|
}
|
|
else if (gAudioIO->IsMonitoring()) {
|
|
gAudioIO->StopStream();
|
|
}
|
|
|
|
// MY: Use routine here so other processes can make same check
|
|
bool bHasTracks = !tracks.empty();
|
|
|
|
// We may not bother to prompt the user to save, if the
|
|
// project is now empty.
|
|
if (!sbSkipPromptingForSave
|
|
&& event.CanVeto()
|
|
&& (settings.EmptyCanBeDirty() || bHasTracks)) {
|
|
if ( UndoManager::Get( project ).UnsavedChanges() ) {
|
|
TitleRestorer Restorer( window, project );// RAII
|
|
/* i18n-hint: The first %s numbers the project, the second %s is the project name.*/
|
|
auto Title = XO("%sSave changes to %s?")
|
|
.Format( Restorer.sProjNumber, Restorer.sProjName );
|
|
auto Message = XO("Save project before closing?");
|
|
if( !bHasTracks )
|
|
{
|
|
Message += XO("\nIf saved, the project will have no tracks.\n\nTo save any previously open tracks:\nCancel, Edit > Undo until all tracks\nare open, then File > Save Project.");
|
|
}
|
|
int result = AudacityMessageBox(
|
|
Message,
|
|
Title,
|
|
wxYES_NO | wxCANCEL | wxICON_QUESTION,
|
|
&window);
|
|
|
|
if (result == wxCANCEL || (result == wxYES &&
|
|
!GuardedCall<bool>( [&]{ return projectFileManager.Save(); } )
|
|
)) {
|
|
event.Veto();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
#ifdef __WXMAC__
|
|
// Fix bug apparently introduced into 2.1.2 because of wxWidgets 3:
|
|
// closing a project that was made full-screen (as by clicking the green dot
|
|
// or command+/; not merely "maximized" as by clicking the title bar or
|
|
// Zoom in the Window menu) leaves the screen black.
|
|
// Fix it by un-full-screening.
|
|
// (But is there a different way to do this? What do other applications do?
|
|
// I don't see full screen windows of Safari shrinking, but I do see
|
|
// momentary blackness.)
|
|
window.ShowFullScreen(false);
|
|
#endif
|
|
|
|
ModuleManager::Get().Dispatch(ProjectClosing);
|
|
|
|
// Stop the timer since there's no need to update anything anymore
|
|
mTimer.reset();
|
|
|
|
// The project is now either saved or the user doesn't want to save it,
|
|
// so there's no need to keep auto save info around anymore
|
|
projectFileIO.AutoSaveDelete();
|
|
|
|
// DMM: Save the size of the last window the user closes
|
|
//
|
|
// LL: Save before doing anything else to the window that might make
|
|
// its size change.
|
|
SaveWindowSize();
|
|
|
|
window.SetIsBeingDeleted();
|
|
|
|
// Mac: we never quit as the result of a close.
|
|
// Other systems: we quit only when the close is the result of an external
|
|
// command (on Windows, those are taskbar closes, "X" box, Alt+F4, etc.)
|
|
bool quitOnClose;
|
|
#ifdef __WXMAC__
|
|
quitOnClose = false;
|
|
#else
|
|
quitOnClose = !projectFileManager.GetMenuClose();
|
|
#endif
|
|
|
|
// DanH: If we're definitely about to quit, clear the clipboard.
|
|
// Doing this after Deref'ing the DirManager causes problems.
|
|
if ((AllProjects{}.size() == 1) &&
|
|
(quitOnClose || AllProjects::Closing()))
|
|
Clipboard::Get().Clear();
|
|
|
|
// JKC: For Win98 and Linux do not detach the menu bar.
|
|
// We want wxWidgets to clean it up for us.
|
|
// TODO: Is there a Mac issue here??
|
|
// SetMenuBar(NULL);
|
|
|
|
projectFileManager.CloseLock();
|
|
|
|
// Some of the AdornedRulerPanel functions refer to the TrackPanel, so destroy this
|
|
// before the TrackPanel is destroyed. This change was needed to stop Audacity
|
|
// crashing when running with Jaws on Windows 10 1703.
|
|
AdornedRulerPanel::Destroy( project );
|
|
|
|
// Destroy the TrackPanel early so it's not around once we start
|
|
// deleting things like tracks and such out from underneath it.
|
|
// Check validity of mTrackPanel per bug 584 Comment 1.
|
|
// Deeper fix is in the Import code, but this failsafes against crash.
|
|
TrackPanel::Destroy( project );
|
|
|
|
// Finalize the tool manager before the children since it needs
|
|
// to save the state of the toolbars.
|
|
ToolManager::Get( project ).Destroy();
|
|
|
|
window.DestroyChildren();
|
|
|
|
TrackFactory::Destroy( project );
|
|
|
|
// Delete all the tracks to free up memory and DirManager references.
|
|
tracks.Clear();
|
|
|
|
// This must be done before the following Deref() since it holds
|
|
// references to the DirManager.
|
|
UndoManager::Get( project ).ClearStates();
|
|
|
|
// Remove self from the global array, but defer destruction of self
|
|
auto pSelf = AllProjects{}.Remove( project );
|
|
wxASSERT( pSelf );
|
|
|
|
if (GetActiveProject() == &project) {
|
|
// Find a NEW active project
|
|
if ( !AllProjects{}.empty() ) {
|
|
SetActiveProject(AllProjects{}.begin()->get());
|
|
}
|
|
else {
|
|
SetActiveProject(NULL);
|
|
}
|
|
}
|
|
|
|
// Since we're going to be destroyed, make sure we're not to
|
|
// receive audio notifications anymore.
|
|
// PRL: Maybe all this is unnecessary now that the listener is managed
|
|
// by a weak pointer.
|
|
if ( gAudioIO->GetListener().get() == &ProjectAudioManager::Get( project ) ) {
|
|
auto active = GetActiveProject();
|
|
gAudioIO->SetListener(
|
|
active
|
|
? ProjectAudioManager::Get( *active ).shared_from_this()
|
|
: nullptr
|
|
);
|
|
}
|
|
|
|
if (AllProjects{}.empty() && !AllProjects::Closing()) {
|
|
|
|
#if !defined(__WXMAC__)
|
|
if (quitOnClose) {
|
|
// Simulate the application Exit menu item
|
|
wxCommandEvent evt{ wxEVT_MENU, wxID_EXIT };
|
|
wxTheApp->AddPendingEvent( evt );
|
|
}
|
|
else {
|
|
sbWindowRectAlreadySaved = false;
|
|
// For non-Mac, always keep at least one project window open
|
|
(void) New();
|
|
}
|
|
#endif
|
|
}
|
|
|
|
window.Destroy();
|
|
|
|
// Destroys this
|
|
pSelf.reset();
|
|
}
|
|
|
|
// PRL: I preserve this handler function for an event that was never sent, but
|
|
// I don't know the intention.
|
|
|
|
void ProjectManager::OnOpenAudioFile(wxCommandEvent & event)
|
|
{
|
|
auto &project = mProject;
|
|
auto &window = GetProjectFrame( project );
|
|
const wxString &cmd = event.GetString();
|
|
|
|
if (!cmd.empty())
|
|
ProjectFileManager::Get( mProject ).OpenFile(cmd);
|
|
|
|
window.RequestUserAttention();
|
|
}
|
|
|
|
// static method, can be called outside of a project
|
|
void ProjectManager::OpenFiles(AudacityProject *proj)
|
|
{
|
|
auto selectedFiles =
|
|
ProjectFileManager::ShowOpenDialog( FileNames::AudacityProjects );
|
|
if (selectedFiles.size() == 0) {
|
|
Importer::SetLastOpenType({});
|
|
return;
|
|
}
|
|
|
|
//first sort selectedFiles.
|
|
selectedFiles.Sort(FileNames::CompareNoCase);
|
|
|
|
auto cleanup = finally( [] {
|
|
Importer::SetLastOpenType({});
|
|
} );
|
|
|
|
for (size_t ff = 0; ff < selectedFiles.size(); ff++) {
|
|
const wxString &fileName = selectedFiles[ff];
|
|
|
|
// Make sure it isn't already open.
|
|
if (ProjectFileManager::IsAlreadyOpen(fileName))
|
|
continue; // Skip ones that are already open.
|
|
|
|
FileNames::UpdateDefaultPath(FileNames::Operation::Open, fileName);
|
|
|
|
// DMM: If the project is dirty, that means it's been touched at
|
|
// all, and it's not safe to open a NEW project directly in its
|
|
// place. Only if the project is brand-NEW clean and the user
|
|
// hasn't done any action at all is it safe for Open to take place
|
|
// inside the current project.
|
|
//
|
|
// If you try to Open a NEW project inside the current window when
|
|
// there are no tracks, but there's an Undo history, etc, then
|
|
// bad things can happen, including data files moving to the NEW
|
|
// project directory, etc.
|
|
if ( proj && (
|
|
ProjectHistory::Get( *proj ).GetDirty() ||
|
|
!TrackList::Get( *proj ).empty()
|
|
) )
|
|
proj = nullptr;
|
|
|
|
// This project is clean; it's never been touched. Therefore
|
|
// all relevant member variables are in their initial state,
|
|
// and it's okay to open a NEW project inside this window.
|
|
proj = OpenProject( proj, fileName );
|
|
}
|
|
}
|
|
|
|
AudacityProject *ProjectManager::OpenProject(
|
|
AudacityProject *pProject, const FilePath &fileNameArg, bool addtohistory)
|
|
{
|
|
AudacityProject *pNewProject = nullptr;
|
|
if ( ! pProject )
|
|
pProject = pNewProject = New();
|
|
auto cleanup = finally( [&] {
|
|
if( pNewProject )
|
|
GetProjectFrame( *pNewProject ).Close(true);
|
|
} );
|
|
ProjectFileManager::Get( *pProject ).OpenFile( fileNameArg, addtohistory );
|
|
pNewProject = nullptr;
|
|
auto &projectFileIO = ProjectFileIO::Get( *pProject );
|
|
if( projectFileIO.IsRecovered() ) {
|
|
auto &window = ProjectWindow::Get( *pProject );
|
|
window.Zoom( window.GetZoomOfToFit() );
|
|
// "Project was recovered" replaces "Create new project" in Undo History.
|
|
auto &undoManager = UndoManager::Get( *pProject );
|
|
undoManager.RemoveStates(1);
|
|
}
|
|
|
|
return pProject;
|
|
}
|
|
|
|
// This is done to empty out the tracks, but without creating a new project.
|
|
void ProjectManager::ResetProjectToEmpty() {
|
|
auto &project = mProject;
|
|
auto &projectFileIO = ProjectFileIO::Get( project );
|
|
auto &projectFileManager = ProjectFileManager::Get( project );
|
|
auto &projectHistory = ProjectHistory::Get( project );
|
|
auto &viewInfo = ViewInfo::Get( project );
|
|
|
|
SelectUtilities::DoSelectAll( project );
|
|
TrackUtilities::DoRemoveTracks( project );
|
|
|
|
// A new DirManager.
|
|
TrackFactory::Reset( project );
|
|
|
|
projectFileManager.Reset();
|
|
|
|
projectHistory.SetDirty( false );
|
|
auto &undoManager = UndoManager::Get( project );
|
|
undoManager.ClearStates();
|
|
}
|
|
|
|
void ProjectManager::RestartTimer()
|
|
{
|
|
if (mTimer) {
|
|
// mTimer->Stop(); // not really needed
|
|
mTimer->Start( 3000 ); // Update messages as needed once every 3 s.
|
|
}
|
|
}
|
|
|
|
void ProjectManager::OnTimer(wxTimerEvent& WXUNUSED(event))
|
|
{
|
|
auto &project = mProject;
|
|
auto &projectAudioIO = ProjectAudioIO::Get( project );
|
|
auto mixerToolBar = &MixerToolBar::Get( project );
|
|
mixerToolBar->UpdateControls();
|
|
|
|
auto gAudioIO = AudioIO::Get();
|
|
// gAudioIO->GetNumCaptureChannels() should only be positive
|
|
// when we are recording.
|
|
if (projectAudioIO.GetAudioIOToken() > 0 && gAudioIO->GetNumCaptureChannels() > 0) {
|
|
wxLongLong freeSpace = ProjectFileIO::Get(project).GetFreeDiskSpace();
|
|
if (freeSpace >= 0) {
|
|
|
|
int iRecordingMins = GetEstimatedRecordingMinsLeftOnDisk(gAudioIO->GetNumCaptureChannels());
|
|
auto sMessage = XO("Disk space remaining for recording: %s")
|
|
.Format( GetHoursMinsString(iRecordingMins) );
|
|
|
|
// Do not change mLastMainStatusMessage
|
|
SetStatusText(sMessage, mainStatusBarField);
|
|
}
|
|
}
|
|
|
|
// As also with the TrackPanel timer: wxTimer may be unreliable without
|
|
// some restarts
|
|
RestartTimer();
|
|
}
|
|
|
|
void ProjectManager::OnStatusChange( wxCommandEvent &evt )
|
|
{
|
|
evt.Skip();
|
|
|
|
auto &project = mProject;
|
|
|
|
// Be careful to null-check the window. We might get to this function
|
|
// during shut-down, but a timer hasn't been told to stop sending its
|
|
// messages yet.
|
|
auto pWindow = ProjectWindow::Find( &project );
|
|
if ( !pWindow )
|
|
return;
|
|
auto &window = *pWindow;
|
|
|
|
window.UpdateStatusWidths();
|
|
|
|
auto field = static_cast<StatusBarField>( evt.GetInt() );
|
|
const auto &msg = ProjectStatus::Get( project ).Get( field );
|
|
SetStatusText( msg, field );
|
|
|
|
if ( field == mainStatusBarField )
|
|
// When recording, let the NEW status message stay at least as long as
|
|
// the timer interval (if it is not replaced again by this function),
|
|
// before replacing it with the message about remaining disk capacity.
|
|
RestartTimer();
|
|
}
|
|
|
|
void ProjectManager::SetStatusText( const TranslatableString &text, int number )
|
|
{
|
|
auto &project = mProject;
|
|
auto pWindow = ProjectWindow::Find( &project );
|
|
if ( !pWindow )
|
|
return;
|
|
auto &window = *pWindow;
|
|
window.GetStatusBar()->SetStatusText(text.Translation(), number);
|
|
}
|
|
|
|
TranslatableString ProjectManager::GetHoursMinsString(int iMinutes)
|
|
{
|
|
if (iMinutes < 1)
|
|
// Less than a minute...
|
|
return XO("Less than 1 minute");
|
|
|
|
// Calculate
|
|
int iHours = iMinutes / 60;
|
|
int iMins = iMinutes % 60;
|
|
|
|
auto sHours = XP( "%d hour", "%d hours", 0 )( iHours );
|
|
|
|
auto sMins = XP( "%d minute", "%d minutes", 0 )( iMins );
|
|
|
|
/* i18n-hint: A time in hours and minutes. Only translate the "and". */
|
|
return XO("%s and %s.").Format( sHours, sMins );
|
|
}
|
|
|
|
// This routine will give an estimate of how many
|
|
// minutes of recording time we have available.
|
|
// The calculations made are based on the user's current
|
|
// preferences.
|
|
int ProjectManager::GetEstimatedRecordingMinsLeftOnDisk(long lCaptureChannels) {
|
|
auto &project = mProject;
|
|
|
|
// Obtain the current settings
|
|
auto oCaptureFormat = QualityPrefs::SampleFormatChoice();
|
|
if (lCaptureChannels == 0) {
|
|
gPrefs->Read(wxT("/AudioIO/RecordChannels"), &lCaptureChannels, 2L);
|
|
}
|
|
|
|
// Find out how much free space we have on disk
|
|
wxLongLong lFreeSpace = ProjectFileIO::Get( project ).GetFreeDiskSpace();
|
|
if (lFreeSpace < 0) {
|
|
return 0;
|
|
}
|
|
|
|
// Calculate the remaining time
|
|
double dRecTime = 0.0;
|
|
double bytesOnDiskPerSample = SAMPLE_SIZE_DISK(oCaptureFormat);
|
|
dRecTime = lFreeSpace.GetHi() * 4294967296.0 + lFreeSpace.GetLo();
|
|
dRecTime /= bytesOnDiskPerSample;
|
|
dRecTime /= lCaptureChannels;
|
|
dRecTime /= ProjectSettings::Get( project ).GetRate();
|
|
|
|
// Convert to minutes before returning
|
|
int iRecMins = (int)round(dRecTime / 60.0);
|
|
return iRecMins;
|
|
}
|