1103 lines
36 KiB
C++
1103 lines
36 KiB
C++
/**********************************************************************
|
|
|
|
Audacity: A Digital Audio Editor
|
|
|
|
ProjectManager.cpp
|
|
|
|
Paul Licameli split from AudacityProject.cpp
|
|
|
|
**********************************************************************/
|
|
|
|
#include "ProjectManager.h"
|
|
|
|
|
|
|
|
#include "AdornedRulerPanel.h"
|
|
#include "AudioIO.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/QualitySettings.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);
|
|
project.Bind( EVT_RECONNECTION_FAILURE,
|
|
&ProjectManager::OnReconnectionFailure, 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
|
|
|
|
#ifdef EXPERIMENTAL_NOTEBOOK
|
|
extern void AddPages( AudacityProject * pProj, GuiFactory & Factory, wxNotebook * pNotebook );
|
|
#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 );
|
|
|
|
// wxGTK3 seems to need to require creating the window using default position
|
|
// and then manually positioning it.
|
|
window.SetPosition(wndRect.GetPosition());
|
|
|
|
auto &projectFileManager = ProjectFileManager::Get( *p );
|
|
|
|
// This may report an error.
|
|
projectFileManager.OpenNewProject();
|
|
|
|
MenuManager::Get( project ).CreateMenusAndCommands( project );
|
|
|
|
projectHistory.InitialState();
|
|
projectManager.RestartTimer();
|
|
|
|
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::OnReconnectionFailure(wxCommandEvent & event)
|
|
{
|
|
event.Skip();
|
|
wxTheApp->CallAfter([this]{
|
|
ProjectWindow::Get(mProject).Close(true);
|
|
});
|
|
}
|
|
|
|
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();
|
|
|
|
// 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.
|
|
auto &clipboard = Clipboard::Get();
|
|
if ((AllProjects{}.size() == 1) &&
|
|
(quitOnClose || AllProjects::Closing()))
|
|
clipboard.Clear();
|
|
else {
|
|
auto clipboardProject = clipboard.Project().lock();
|
|
if ( clipboardProject.get() == &mProject ) {
|
|
// Closing the project from which content was cut or copied.
|
|
// For 3.0.0, clear the clipboard, because accessing clipboard contents
|
|
// would depend on a database connection to the closing project, but
|
|
// that connection should closed now so that the project file can be
|
|
// freely moved.
|
|
// Notes:
|
|
// 1) maybe clipboard contents could be saved by migrating them to
|
|
// another temporary database, but that extra effort is beyond the
|
|
// scope of 3.0.0.
|
|
// 2) strictly speaking this is necessary only when the clipboard
|
|
// contains WaveTracks.
|
|
clipboard.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);
|
|
|
|
// Compact the project.
|
|
projectFileManager.CompactProjectOnClose();
|
|
|
|
// Set (or not) the bypass flag to indicate that deletes that would happen during
|
|
// the UndoManager::ClearStates() below are not necessary.
|
|
projectFileIO.SetBypass();
|
|
|
|
{
|
|
// This can reduce reference counts of sample blocks in the project's
|
|
// tracks.
|
|
UndoManager::Get( project ).ClearStates();
|
|
|
|
// Delete all the tracks to free up memory
|
|
tracks.Clear();
|
|
}
|
|
|
|
// 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();
|
|
|
|
// Close project only now, because TrackPanel might have been holding
|
|
// some shared_ptr to WaveTracks keeping SampleBlocks alive.
|
|
// We're all done with the project file, so close it now
|
|
projectFileManager.CloseProject();
|
|
|
|
WaveTrackFactory::Destroy( project );
|
|
|
|
// 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)
|
|
{
|
|
const wxString &cmd = event.GetString();
|
|
if (!cmd.empty()) {
|
|
ProjectChooser chooser{ &mProject, true };
|
|
if (auto project = ProjectFileManager::OpenFile(
|
|
std::ref(chooser), cmd)) {
|
|
auto &window = GetProjectFrame( *project );
|
|
window.RequestUserAttention();
|
|
chooser.Commit();
|
|
}
|
|
}
|
|
}
|
|
|
|
// static method, can be called outside of a project
|
|
void ProjectManager::OpenFiles(AudacityProject *proj)
|
|
{
|
|
auto selectedFiles =
|
|
ProjectFileManager::ShowOpenDialog(FileNames::Operation::Open);
|
|
if (selectedFiles.size() == 0) {
|
|
Importer::SetLastOpenType({});
|
|
return;
|
|
}
|
|
|
|
//first sort selectedFiles.
|
|
selectedFiles.Sort(FileNames::CompareNoCase);
|
|
|
|
auto cleanup = finally( [] {
|
|
Importer::SetLastOpenType({});
|
|
} );
|
|
|
|
for (const auto &fileName : selectedFiles) {
|
|
// Make sure it isn't already open.
|
|
if (ProjectFileManager::IsAlreadyOpen(fileName))
|
|
continue; // Skip ones that are already open.
|
|
|
|
proj = OpenProject( proj, fileName,
|
|
true /* addtohistory */, false /* reuseNonemptyProject */ );
|
|
}
|
|
}
|
|
|
|
bool ProjectManager::SafeToOpenProjectInto(AudacityProject &proj)
|
|
{
|
|
// DMM: If the project is dirty, that means it's been touched at
|
|
// all, and it's not safe to open a fresh project directly in its
|
|
// place. Only if the project is brandnew 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 fresh project inside the current window when
|
|
// there are no tracks, but there's an Undo history, etc, then
|
|
// bad things can happen, including orphan blocks, or tracks
|
|
// referring to non-existent blocks
|
|
if (
|
|
ProjectHistory::Get( proj ).GetDirty() ||
|
|
!TrackList::Get( proj ).empty()
|
|
)
|
|
return false;
|
|
// 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 its window.
|
|
return true;
|
|
}
|
|
|
|
ProjectManager::ProjectChooser::~ProjectChooser()
|
|
{
|
|
if (mpUsedProject) {
|
|
if (mpUsedProject == mpGivenProject) {
|
|
// Ensure that it happens here: don't wait for the application level
|
|
// exception handler, because the exception may be intercepted
|
|
ProjectHistory::Get(*mpGivenProject).RollbackState();
|
|
// Any exception now continues propagating
|
|
}
|
|
else
|
|
GetProjectFrame( *mpUsedProject ).Close(true);
|
|
}
|
|
}
|
|
|
|
AudacityProject &
|
|
ProjectManager::ProjectChooser::operator() ( bool openingProjectFile )
|
|
{
|
|
if (mpGivenProject) {
|
|
// Always check before opening a project file (for safety);
|
|
// May check even when opening other files
|
|
// (to preserve old behavior; as with the File > Open command specifying
|
|
// multiple files of whatever types, so that each gets its own window)
|
|
bool checkReuse = (openingProjectFile || !mReuseNonemptyProject);
|
|
if (!checkReuse || SafeToOpenProjectInto(*mpGivenProject))
|
|
return *(mpUsedProject = mpGivenProject);
|
|
}
|
|
return *(mpUsedProject = New());
|
|
}
|
|
|
|
void ProjectManager::ProjectChooser::Commit()
|
|
{
|
|
mpUsedProject = nullptr;
|
|
}
|
|
|
|
AudacityProject *ProjectManager::OpenProject(
|
|
AudacityProject *pGivenProject, const FilePath &fileNameArg,
|
|
bool addtohistory, bool reuseNonemptyProject)
|
|
{
|
|
ProjectManager::ProjectChooser chooser{ pGivenProject, reuseNonemptyProject };
|
|
if (auto pProject = ProjectFileManager::OpenFile(
|
|
std::ref(chooser), fileNameArg, addtohistory )) {
|
|
chooser.Commit();
|
|
|
|
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(0, 1);
|
|
}
|
|
return pProject;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
// 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 );
|
|
|
|
WaveTrackFactory::Reset( project );
|
|
|
|
projectHistory.SetDirty( false );
|
|
auto &undoManager = UndoManager::Get( project );
|
|
undoManager.ClearStates();
|
|
|
|
projectFileManager.CloseProject();
|
|
projectFileManager.OpenProject();
|
|
}
|
|
|
|
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 = QualitySettings::SampleFormatChoice();
|
|
if (lCaptureChannels == 0)
|
|
lCaptureChannels = AudioIORecordChannels.Read();
|
|
|
|
// 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;
|
|
}
|